一、题目大意
我们在玩一个猜数字的游戏,游戏规则如下:
我从1到n选择一个数字,你来猜我选择的数字。
每次你猜错,我都会告诉真实值是偏大还是偏小。
然而,当你猜了一个数字x,并且猜错的时候,你必须支付$x给我,当你猜到我选择的数字时,你就可以赢得比赛。
例如:
n=10,我选择了8
第一轮:你猜5,我告诉你真实值大于该值,你支付$5;
第二轮:你猜7,我告诉你真实值大于该值,你支付$7;
第三轮:你猜9,我告诉你真实值小于该值,你支付$9;
游戏结束,我选择的数字是8 。
你最终支付了$5+$7+$9=$21。
给定一个值
n≥1
,求为了保证赢得比赛,你必须有多少钱。
二、编程思路
2.1、2.2节为不成功的编程思路,关心答案的同学可以直接转到2.3即可。
2.1 初刻拍案惊奇
乍一看此题可以使用二分法求解。
n=1,{1},ans=0;
n=2,{1,2},ans=1;
n=3,{1,2,3},ans=2;
n=4,{1,2,3,4},ans=min{2+3,3+1}=4;
…
似乎没问题,令f(x,y)表示猜[x,y]范围内所需要花的钱,
med=x+y2
,若med为整数,则先猜med;若med不是整数,则猜
medf=floor(med)
与
medc=ceil(med)
两种情况,取其中情况较小的。
总结其递推公式如下:
- f(x,y)=0;
- f(x,y)=x, y=x+1;
- f(x,y)=med+f(x,med−1)+f(med+1,y), medc==medf
- f(x,y)=min{medc+f(x,medc−1)+f(medc+1,y),medf+f(x,medf−1)+f(medf+1,y)}, medc!=medf
但是当n==5时,根据二分法求出来的是:n=5,{1,2,3,4,5},f(1,5)=3+4=7,而正确的结果应该是f(1,5)=2+4=6。
二分法宣告失败,这是因为二分法只能够找到猜的次数最少的,而不能够保证所猜的数字和最小。
2.2 二刻拍案惊奇
既然二分法不行,那就使用动态规划思路进行求解,继续找规律。
n=1,{1},ans=0;
n=2,{1,2},ans=min{1,2}=1;
n=3,{1,2,3},ans=min{1+2,2,3+1};
枚举第一次分别猜1 2 3的情况。
n=4,{1,2,3,4},ans=min{1+{2,3,4}, 2+{3,4}+{1},3+{1,2}+{4}, 4+{1,2,3}}=4;
…
n,{1,2,3,…,n},ans=MINi=ni=1{i+max({1,2,3,…,i−1}, {i+1,…,n}}
,即分别枚举猜1
…
n的情况,从而将问题分解为左右两个子序列的问题。即上式中的
{1,2,3,…,i−1}与{i+1,…,n}
,然后分别求其需要的钱数即可。
至此,思路还没有出现问题。
经过初步观察似乎可以发现,右边序列需要的钱数
{i+1,…,n}?=(似乎等于)i+{1,…,n−i}
于是总结出如下递推公式:
- 令dp[i]表示数字范围为1…i之间时,为了保证猜中数字所需要的最少钱数,则
- dp[0]=0;
- dp[1]=0;
- dp[2]=1;
- dp[i]=MINk=ik=1(k+max{dp[k−1],dp[i−j]+((i−j)<2?0:j)})
2.3 三刻拍案惊奇
以上思路中,右边序列需要的钱数
{i+1,…,n}?=(似乎等于)i+{1,…,n−i}
属于根据直觉得出,与正确结果对比发现,当n
≥
12时,会出现错误。其实只有当子序列中只猜一次时,以上关系才成立。
继续使用动态规划思路,令dp[i][j]猜中[i,j]范围之内数字时所需要最少钱数。则递推公式很容易获得,
dp[i][j]=MINk=jk=i{k+max{dp[i][k−1],dp[k+1][j]}}
三、代码设计
class Solution {
public:
int getMoneyAmount(int n) {
if(n ==0) return 0;
vector<vector<int> > dp(n+1, vector<int>(n+1, 0));
//注意此处的迭代方向
for(int i = n-1; i > 0; i--)
{
for(int j = i+1; j <=n; j++)
{
int ans = 0xfffffff;
for(int k = i; k <j; k++)
ans = min(ans, k + max(dp[i][k-1], dp[k+1][j]));
dp[i][j] = ans;
}
}
return dp[1][n];
}
};