其他分享
首页 > 其他分享> > 【题解】P3147 [USACO16OPEN]262144 P(DP)

【题解】P3147 [USACO16OPEN]262144 P(DP)

作者:互联网

【题解】P3147 [USACO16OPEN]262144 P

虽然是道绿题,但我还是挂/卡了很久,是道好题,写篇题解记录一下。


题目链接

P3147 [USACO16OPEN]262144 P - 洛谷

P3146 [USACO16OPEN]248 G - 洛谷

下面这道题是上面的弱化版,只是数据范围不同,但有一种解法只能通过 P3147,所以这里都以 P3147 为例解释(P3147 的 AC 代码可以通过 P3146),但是会讲到两种解法。

题意概述

有一个长为 \(n(2 \le n \le 262144)\) 的正整数序列,范围在 \([1,40]\)。在一步中,可以选择相邻的两个相同的数,然后合并成一个比原来大一的数(例如两个 \(7\) 合成一个 \(8\)),最大化出现过的最大的数。

思路分析

真的是道妙题,单从线性 DP 的角度思考我愣是想不出来 DP 状态;而从区间 DP 的角度想又做法假了。这里记录线性 DP区间 DP 两种解法。

解法一:线性 DP

要合成一个数 \(i\),那么就要找到两个相邻的最大值为 \(i-1\) 的区间。

定义 \(dp_{i,j}\) 表示左端点为 \(j\),能合并出 \(i\) 的右端点的位置。

发现这玩意跟倍增有点相似。

那么有:

\[dp[i][j]=dp[i-1][dp[i-1][j]] \]

画个图解释一下:

相当于是从 \(j\) 开始找到合并出 \(i-1\) 的右端点位置,再从这个右端点位置往后找到从这个位置开始能合并出 \(i-1\) 的第二个右端点位置。

那么就相当于找到了两段相邻的最大值为 \(i-1\) 的区间,可以合并出 \(i\)。

这么妙的做法我怎么就想不到呢。/kk

那么我们只需要在刚开始输入时,初始化 \(dp[a_i][i]=i+1\)。(注意不是 \(i\) 而是 \(i+1\))

然后枚举 \(i\),在这道题中 \(a_i\) 最大到 \(40\),这种倍增式合并,能合成的最大的 \(i\) 为 \(40+ \log n=40+ \log 262144 \approx 40+18=58\),所以只需要把 \(i\) 从 \(1\) 枚举到 \(58\) 即可。

最后枚举 \(j\),\(j\) 从 \(1-n\)。

时间复杂度:\(O(58 n)\)。

解法二:区间 DP

注:这种解法是针对 P3146 的,由于 P3147 的数据范围较大,此种做法行不通。

相对于这个题的线性 DP,区间 DP 是更好想的。

看到“合并”,应该一眼想到区间 DP。

我当时心想:这题如果用区间 DP 未免也太简单了,然后立马开写,结果却 WA 了,只有 \(58pts\)……我真菜。。

这里记录一下我的思路历程。

刚开始想,这题不就是石子合并(弱化版) - 洛谷的变形嘛。

用 \(dp_{l,r}\) 表示区间 \([l,r]\) 所能合成的最大值,在求解 \(dp_{l,r}\) 时,枚举断点 \(k\),然后分类讨论。

最后答案是 \(dp_{1,n}\)。但是这样交上去却 WA 了,只有 \(58pts\),然后看了眼评论区发现我的做法假了。

Hack:

3
2 1 2

按照我上述思路:

在求 \(dp_{1,3}\) 时,由于 \(dp_{1,1}=dp_{2,3}=2\),所以 \(dp_{1,3}=3\),但实际答案应该是 \(2\)。

因为 \(dp_{2,3}\) 的答案是继承了 \(dp_{3,3}\),而这样的继承在这一次转移中是可以的,但当用 \(dp_{2,3}\) 去计算 \(dp_{1,3}\) 却由于 \(1\) 和 \(3\) 不连续,所以得到的答案是错误的。

那么怎么做呢。

可以用一个变量 \(ans\) 表示当前找到的最大值答案。

对于每个 \(dp_{l,r}\),只有当 \(dp_{l,k}=dp_{k+1,r}\) 的时候更新值,然后每次将 \(ans\) 更新为当前最大即可。

注意 \(ans\) 在初始输入时也要更新一遍。

时间复杂度:

代码实现

解法一

//luoguP3147
//线性 DP 解法 
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=262150;
int a[maxn],dp[65][maxn];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
 } 
 
int main()
{
	int n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		dp[a[i]][i]=i+1;
	}
	int ans=0;
	for(int i=2;i<=58;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(!dp[i][j])dp[i][j]=dp[i-1][dp[i-1][j]];
			if(dp[i][j])ans=i;
		}
	}
	cout<<ans<<endl;
	return 0; 
}
/*dp[i][j] 表示左端点为 j,能合并出 i 的右端点位置。 
那么 dp[i][j]=dp[i-1][dp[i-1][j]].
可以发现数字大小最大为 40+log n = 58.
这题怎么这么妙啊!我为什么就想不到! 
*/ 

解法二

//luoguP3146
//区间 DP 解法
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=255;
int dp[maxn][maxn],a[maxn];
int ans;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
 } 
 
int main()
{
	int n;
	n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();dp[i][i]=a[i];
		ans=max(ans,a[i]);
	}
	for(int len=2;len<=n;len++)
	{
		for(int l=1;l+len-1<=n;l++)
		{
			int r=l+len-1;
			for(int k=l;k<r;k++)
			{
				if(dp[l][k]==dp[k+1][r])dp[l][r]=max(dp[l][r],dp[l][k]+1);
				ans=max(dp[l][r],ans);
			}
//			cout<<l<<" "<<r<<" "<<dp[l][r]<<endl; 
		}
	}
	cout<<ans<<endl;
	return 0;
}
/*3
2 1 2
*/

标签:ch,DP,int,题解,USACO16OPEN,区间,解法,dp
来源: https://www.cnblogs.com/xrkforces/p/luogu-P3147.html