《算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge。
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。
【题目描述】 小蓝和小红在玩石头剪刀布的游戏,为方便描述,分别用数字1、2、3表示石头、剪刀、布。
游戏进行N轮,小蓝已经知道小红每一轮要出的手势,但是小蓝很懒,最多变换K次手势。
请求出小蓝最多赢的次数。。
【输入格式】 第一行输入两个数字N和K。(1≤N≤100000,0≤K≤20)
第二行输入N个数字表示小红每一轮输出的手势。。
【输出格式】 输出一个数字表示答案。。
【输入样例】
5 1
3 3 1 3 2
【输出样例】
4
读者可以试试用暴力或贪心是否能够求解。当贪心不能得到全局最优解时,常常能用DP求解。
定义状态dp[i][j][k],表示小蓝前i轮变换了j次,当前手势为k的最大的胜利次数。计算结束后,答案是max{dp[n][k][1], dp[n][k][2],dp[n][k][3]}。
下面推导状态转移方程。考察小蓝的连续2次手势,有2种情况:不变、变了。
(1)手势不变。那么变换次数j没有变。
小蓝是否能赢?设小蓝的上一次手势为k,新手势为nk,由于手势不变有nk = k。nk能否赢a[i]?用check(nk, a[i])判断能赢的几种情况即可。状态转移方程的代码这样写:
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j][k] + check(nk,a[i]));
(2)手势变了。那么dp[i][j][]从dp[i-1][j-1][]递推而来。同样用check(nk, a[i])判断能赢的几种情况。状态转移方程的代码:
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j-1][k] + check(nk,a[i]));
【重点】 动态规划、滚动数组。
下面先给出不用滚动数组的代码。第6行定义状态int dp[100010][25][4],使用空间40M。
#include
using namespace std;
int dp[100010][25][4];
int a[100010];
int check(int a, int b) { //几种赢的情况
if (a == 1 && b == 2) return 1;
if (a == 2 && b == 3) return 1;
if (a == 3 && b == 1) return 1;
return 0;
}
int main(){
int N, K; cin >> N >> K;
K += 1;
for(int i = 1; i <= N; i++) cin >> a[i];
for(int i = 1; i <= N; i++) { //n次游戏
for(int j = 1; j <= K; j++) //变换的次数
for(int k = 1; k <= 3; k++){ //小蓝上一轮的手势
for(int nk = 1; nk <= 3; nk++){ //小蓝当前的手势
if(k == nk) //手势不需要变化,那么j不变
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j][k] + check(nk,a[i]));
else //手势需要变化,那么j减1
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j-1][k] + check(nk,a[i]));
}
}
}
cout<<max(dp[N][K][1], max(dp[N][K][2], dp[N][K][3]))<<endl;
return 0;
}
其中的check()函数,可以简化为:nk == a[i] % 3 + 1。重写代码如下:
#include
using namespace std;
int dp[100010][25][4];
int a[100010];
int main(){
int N, K; cin >> N >> K;
K += 1;
for(int i = 1; i <= N; i++) cin >> a[i];
for(int i = 1; i <= N; i++) { //n次游戏
for(int j = 1; j <= K; j++) //变换的次数
for(int k = 1; k <= 3; k++){ //小蓝上一轮的手势
for(int nk = 1; nk <= 3; nk++){ //小蓝当前的手势
if(k == nk) //手势不需要变化,那么j不变
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j][k] + (nk == a[i] % 3 + 1));
else //手势需要变化,那么j减1
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j-1][k] + (nk == a[i] % 3 + 1));
}
}
}
cout<<max(dp[N][K][1], max(dp[N][K][2], dp[N][K][3]))<<endl;
return 0;
}
可以用滚动数组优化空间。下面给出交替滚动的一种实现:用i%2和(i-1)%2交替滚动。第3行定义状态int dp[2][25][4],使用空间800字节。
#include
using namespace std;
int dp[2][25][4]; //滚动数组 ///滚动数组
int a[100010];
int main(){
int N, K; cin >> N >> K;
K += 1;
for(int i = 1; i <= N; i++) cin >> a[i];
for(int i = 1; i <= N; i++) { //n次游戏
for(int j = 1; j <= K; j++) //变换的次数
for(int k = 1; k <= 3; k++){ //小蓝上一轮的手势
for(int nk = 1; nk <= 3; nk++){ //小蓝当前的手势
if(k == nk) //手势不需要变化,那么j不变
//当前手势为k,能否赢a[i]:只需要判断当前手势编号是否等于之前的编号即可
dp[i%2][j][nk] = max(dp[i%2][j][nk], dp[(i-1)%2][j][k] + (nk == a[i] % 3 + 1));
else //手势需要变化,那么j减1
dp[i%2][j][nk] = max(dp[i%2][j][nk], dp[(i-1)%2][j-1][k] + (nk == a[i] % 3 + 1));
}
}
}
cout<<max(dp[N%2][K][1], max(dp[N%2][K][2], dp[N%2][K][3]))<<endl;
return 0;
}
下面是“交替滚动”的另一种实现,见《算法竞赛》P323页“交替滚动”:用old和now交替滚动。
#include
using namespace std;
int dp[2][25][4]; //滚动数组
int a[100010];
int main(){
int N, K; cin >> N >> K;
K += 1;
for(int i = 1; i <= N; i++) cin >> a[i];
int now = 0,old = 1; //交替滚动
for(int i = 1; i <= N; i++) { //n次游戏
swap(old,now);
for(int j = 1; j <= K; j++) //变换的次数
for(int k = 1; k <= 3; k++){ //小蓝上一轮的手势
for(int nk = 1; nk <= 3; nk++){ //小蓝当前的手势
if(k == nk) //手势不需要变化,那么j不变
//当前手势为k,能否赢a[i]:只需要判断当前手势编号是否等于之前的编号即可
dp[now][j][nk] = max(dp[i%2][j][nk], dp[old][j][k] +(nk == a[i] % 3 + 1));
else //手势需要变化,那么j减1
dp[now][j][nk] = max(dp[i%2][j][nk], dp[old][j-1][k] + (nk == a[i] % 3 + 1));
}
}
}
cout<<max(dp[now][K][1], max(dp[now][K][2], dp[now][K][3]))<<endl;
return 0;
}
本题的滚动数组能否用“自我滚动”?请读者思考。
import java.util.*;
public class Main {
static int[][][] dp = new int[2][25][4]; // 滚动数组
static int[] a = new int[100010];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int K = scanner.nextInt();
K += 1;
for (int i = 1; i <= N; i++) a[i] = scanner.nextInt();
int now = 0, old = 1; // 交替滚动
for (int i = 1; i <= N; i++) { // n次游戏
int temp = now;
now = old;
old = temp;
for (int j = 1; j <= K; j++) { // 变换的次数
for (int k = 1; k <= 3; k++) { // 小蓝上一轮的手势
for (int nk = 1; nk <= 3; nk++) { // 小蓝当前的手势
if (k == nk) { // 手势不需要变化,那么j不变
dp[now][j][nk] = Math.max(dp[now][j][nk], dp[old][j][k] + (nk == a[i] % 3 + 1 ? 1 : 0));
} else { // 手势需要变化,那么j减1
dp[now][j][nk] = Math.max(dp[now][j][nk], dp[old][j-1][k] + (nk == a[i] % 3 + 1 ? 1 : 0));
}
}
}
}
}
System.out.println(Math.max(dp[now][K][1], Math.max(dp[now][K][2], dp[now][K][3])));
}
}
。
N, K = map(int, input().split())
K += 1
a = [0] + list(map(int, input().split()))
dp = [[[0 for _ in range(4)] for _ in range(25)] for _ in range(2)] # 滚动数组
old, now = 0, 1 # 交替滚动
for i in range(1, N + 1): # n次游戏
old, now = now, old
for j in range(1, K + 1): # 变换的次数
for k in range(1, 4): # 小蓝上一轮的手势
for nk in range(1, 4): # 小蓝当前的手势
if k == nk: # 手势不需要变化,那么j不变
dp[now][j][nk] = max(dp[now][j][nk], dp[old][j][k] + (nk == a[i] % 3 + 1))
else: # 手势需要变化,那么j减1
dp[now][j][nk] = max(dp[now][j][nk], dp[old][j-1][k] + (nk == a[i] % 3 + 1))
print(max(dp[now][K][1], dp[now][K][2], dp[now][K][3]))