题目分析
题意给出了面对金币数为奇数和偶数时的不同处理方式,偶数时能够选择拿走一半或一颗,而奇数时只能选择拿走一颗。小R先开始,小R和小S都希望获得最多的金币。
思路
由于一开始给出的样例数量较小,因此收到之前拿最后一根火柴类型的题目的影响,认为要始终让下一个人拿到奇数个的金币才能保证自己获得的金币数是最多的。因此写了以下代码,当小R面对的是奇数的时候只有一种选择,当小R面对是偶数的时候,如果拿走之后的金币数仍为偶数则选择只拿一个确认下一个人拿到的时候为奇数。
提交报错AI检查
public class Main {
public static int[] solution(int N) {
// write code here
int Rnum = 0;
int Snum = 0;
while(N>0){
if(N%2==0){
if((N/2)%2!=0){
Rnum=Rnum+N/2;
N=N/2;
}else{
Rnum++;
N=N-1;
}
}else{
Rnum++;
N=N-1;
}
if(N%2==0&&N>0){
if((N/2)%2!=0){
Snum=Snum+N/2;
N=N/2;
}else{
Snum++;
N=N-1;
}
}else{
Snum++;
N=N-1;
}
}
return new int[Rnum,Snum];
}
}
通过提交发现有错误,经过AI检查代码后了解到,有以下两个问题。
数组初始化
在 return new int[Rnum,Snum],该代码有错误,应该写成return new int []{Rnum,Snum}
代码不够简洁
AI指出S和R之间的重复逻辑多,可以进行合并,因此可以使用一个布尔类型的变量进行标记来规定目前拿金币的人员是谁,进而优化代码结构,使得代码更加浅显易懂。
思路修改
在修改上述内容之后,发现当N的数值变大的时候,这种方法并不能保证R或者S获得最大的数量,反而会趋近于两者平均,因此再次思考题目的思路是否有问题。
考虑到本题为博弈问题,两人都希望能够获得最优策略,因此可以采取动态规划问题。
动态规划
动态规划的核心思想:
动态规划的核心思想是 将问题拆解成子问题,并保存这些子问题的结果,以便以后使用。这种方法避免了重复计算,从而大大提高了算法的效率。
具体来说,动态规划适用于以下两种情况:
- 最优子结构:问题的最优解可以由子问题的最优解组合而成。
- 子问题重叠:在解决问题的过程中,很多子问题会被多次计算,动态规划通过保存子问题的结果来避免重复计算。
动态规划的步骤:
- 定义状态:定义一个数组或表格来记录子问题的结果。每个状态表示在某个子问题下的最优解。
- 状态转移方程:确定如何从已经解决的子问题中得到当前问题的解。
- 初始化:设置初始状态,通常是最小的子问题,通常这些状态很容易直接解决。
- 计算顺序:根据状态转移方程计算各个子问题的最优解,通常是从最简单的子问题开始逐步解决。
- 返回结果:最终,我们通过状态转移得到的结果即为原问题的最优解。
代码实现
通过逐步计算每个剩余金币数的情况,动态规划算法从最小的状态开始计算,逐步推导出更大的状态,最终得出最优解。这样我们就避免了重复计算。
int[][] dp = new int[N + 1][2]; // dp[i][0]是小R的金币数,dp[i][1]是小S的金币数
dp[0][0] = dp[0][1] = 0;
for (int i = 1; i <= N; i++) {
if (i % 2 == 1) {
dp[i][0] = dp[i - 1][1] + 1;
dp[i][1] = dp[i - 1][0];
} else {
int takeOneR = dp[i - 1][1] + 1;
int takeOneS = dp[i - 1][0];
int takeHalfR = dp[i / 2][1] + i / 2;
int takeHalfS = dp[i / 2][0];
if (takeOneR > takeHalfR) {
dp[i][0] = takeOneR;
dp[i][1] = takeOneS;
} else {
dp[i][0] = takeHalfR;
dp[i][1] = takeHalfS;
}
}
}
return new int[] {dp[N][0], dp[N][1]};
dp[i] 是一个数组,保存剩余 i 枚金币时,小R和小S分别获得的金币数
初始化
dp[0] = {0, 0} 表示没有金币时,两个人都不能得到金币
从1到N逐步计算每个状态下小R和小S的金币数
返回最终结果,dp[N] 包含小R和小S各自能获得的金币数
总结
通过本题了解到了动态规划思想,同时借助AI找到了自己代码知识上的缺漏,动态规划问题通过从数量小的简单问题开始思考,采用逆向思维,保存之前的问题解决方法,从而在之后的计算能够调用之前计算的方法,因此能够保证是最好的解决方法。