【动态规划】【换维】扔鸡蛋游戏

【动态规划】【换维】扔鸡蛋游戏

这是一道在《信息学奥赛一本通》上的经典题目。题目描述如下:

\(k\) 个一模一样的鸡蛋,楼的高度为 \(n\) ,定义鸡蛋的硬度为 \(x\) ,当且仅当将鸡蛋从 \(x\) 楼扔下不会碎,从 \(x + 1\) 楼扔下会碎,求最坏情况下求出鸡蛋硬度的最小步数。

\(k \leq 10,n \leq 100\) 。保证硬度在 \([0,n]\) 之间。

考虑假设只有两个蛋,最小步数是 \(t\) ,我们的策略是怎样的:

image

我们一定是分块来扔,考虑如果第一次碎了,那么硬度一定小于第一次扔的高度,此时我们还有 \(t - 1\) 次,可以确定 \(t - 1\) 个位置(扔 \(1\)\(t - 1\) ,如果 \(1\) 碎了就是 \(0\)),所以第一次的高度选为 \(t\) 。后面同理,只不过每次都比上一次少了 \(1\) 。最后能确定的高度上限是 \(\frac {t(t + 1)}2\)

接下来假设我们有 \(3\) 个蛋,假设第一次高度为 \(x\) 。如果碎了,我们就要用 \(2\) 个蛋和 \(t - 1\) 次来确定剩下的高度,假设 \(f_{i,j}\)\(i\) 个蛋 \(j\) 次能达到的高度。这个高度 \(x\) 就等于 \(f_{2,t - 1} + 1\) 。如果没碎,我们就要用 \(3\) 个蛋和 \(t - 1\) 次确定上面的高度,所以是 \(f_{3,t - 1}\)

综上所述, \(dp\) 式子已经很完全了,就是:

\[f_{i,j} = f_{i - 1,j - 1} + 1 + f_{i,j - 1} \]

直接做即可,这道题目难以通过枚举高度和鸡蛋个数来算出次数,所以我们考虑换一维,用个数和次数推出最高高度,再通过二分回答询问,是一种很巧妙的思路,时间复杂度 \(\Theta(kn)\)

当然还有基于枚举高度的 \(\Theta(n^2k)\) 的做法,设 \(f_{i,j}\) 为硬度在 \([0,i]\) 之间,还有 \(j\) 个鸡蛋时的最小次数,我们每次枚举中间在哪个位置扔一次,答案就是碎和不碎两种情况的 \(max\) ,再将所有枚举的情况取 \(min\)

\[dp_{i,j} = \min \{dp_{i,j},\max\{dp_{k - 1,j - 1},dp_{i - k,j}\} + 1\} \]

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,dp[101][101];//dp[i][j]:硬度在[0,i]之间,还有J个鸡蛋
int main()
{
	while(cin>>n>>m)
	{
		memset(dp,0x3f,sizeof(dp));
		for(int i=1;i<=n;i++)
			dp[i][1]=i;//只有一个鸡蛋,扔i次
		for(int i=1;i<=m;i++)
			dp[1][i]=dp[0][i]=1;//硬度只有2种情况,只扔1次
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				for(int k=1;k<=i;k++)
					dp[i][j]=min(dp[i][j],max(dp[k-1][j-1],dp[i-k][j])+1);
		cout<<dp[n][m]<<endl;
	}
	return 0;
 }

如果这题数据加强到 \(10^{18}\) 我们怎么做呢?

考虑到 \(k \geq \lceil\log n\rceil\) 时,我们只需要二分来扔鸡蛋,就是最优选择。

鸡蛋数很少时,我们沿用第一种 \(dp\) 方程,发现 \(f_i(j)\) 是一个关于 \(j\) 的多项式,由于每次 \(f_i\) 近似于 \(f_{i - 1}\) 的前缀和,所以每次次数增加 \(1\) ,这是一个 \(i\) 次多项式,我们求的多项式次数很小,只有大约 \(64\) 次,但是自变量很大,可以达到 \(10^{18}\) 。提前计算次数 \(+ 1\) 个值后每次询问拉格朗日插值即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 105;
typedef long long ll;
const ll inf = 1000000000000000005;
long long f[N][N];
inline void prework()
{
	for(int i = 1;i <= 60;i++) f[1][i] = i;
	for(int i = 2;i <= 58;i++)
	{
		f[i][1] = 1;
		for(int j = 2;j <= 60;j++)
			f[i][j] = f[i - 1][j - 1] + 1 + f[i][j - 1];
	}
} 
inline long double lag(ll p,ll X)
{
	long double jdg = 0;
	for(int i = 1;i <= p + 1;i++)
	{
		long double now = f[p][i];
		for(int j = 1;j <= p + 1;j++) 
		{
			if(j == i) continue;
			now *= 1.00 * (X - j) / (i - j);
		}
		jdg += now;
		if(jdg > inf) return inf;
	}
	return jdg;
}
inline ll lg(ll x)
{
	ll ret = 1;
	if((x & (-x)) == x) --ret;
	for(;x;x /= 2) ++ret;
	return ret;
}
int main()
{
	long long n,k;
	prework();
	while(cin>>n>>k)
	{
		if(k == 1) {cout<<n<<endl; continue;}
        if(k >= 59)
    	{
    		cout<<lg(n);
	    	continue;
	    }
		int l = 1,r = 4000005;
		while(l < r)
		{
			int mid = (l + r) >> 1;
			if(lag(k,mid) < n) l = mid + 1;
			else r = mid;
		}
		cout<<l<<endl; 
	}
	return 0;
}

之所以二分上界是 \(4 \times 10^6\) 是因为 \(k \geq 2\) 时答案不会超过这个数字,插值时如果当前答案大于 \(inf\) 就直接返回 \(inf\) 因为最终要做的只是比大小。

当然这题可以前缀和优化 \(dp\)\(\Theta (n)\) ,然后直接 \(dp\)\(4 \times 10^6\) ,只有 \(k = 2\) 时会接近这个数字,其他的都远小于,所以也可以做。

posted @ 2023-09-14 22:12  The_Last_Candy  阅读(22)  评论(0编辑  收藏  举报