Living-Dream 系列笔记 第48期
区间 dp:以区间为子问题的 dp。
特征:
- 
从左往右 / 从右往左递推会得到不同结果; 
- 
常为 合并类 / 拆分类 / 处理两端类; 
- 
要么枚举中间断点,要么枚举两端点。 
T1
令 \(dp_{i,j}\) 表示合并区间 \([i,j]\) 所需代价。易得答案为 \(dp_{1,n}\)。
初始状态即为 \(dp_{i,i}=0\),其余为 \(\infty\)。
考虑枚举中转点 \(k\),将区间 \([i,j]\) 分为区间 \([i,k]\) 与 \([k+1,j]\) 分别进行合并。
因此有转移:
(\(sum_i\) 为 \(m_i\) 的前缀和)
\(O(n^3)\) 转移即可。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e2+5;
int n,a[N],sum[N];
int dp[N][N];
signed main(){
	ios::sync_with_stdio(0);
	cin>>n;
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=n;i++)
		cin>>a[i],sum[i]=sum[i-1]+a[i],dp[i][i]=0;
	for(int i=2;i<=n;i++){
		for(int j=1;j+i-1<=n;j++){
			int s=j,e=j+i-1;
			for(int k=s;k<=e-1;k++)
				dp[s][e]=min(dp[s][e],dp[s][k]+dp[k+1][e]+sum[e]-sum[s-1]);
		}
	}
	cout<<dp[1][n];
	return 0;
}
T2
令 \(dp_{i,j}\) 表示合并区间 \([i,j]\) 所能获得的最大数。易得答案为 \(dp_{1,n}\)。
初始状态即为 \(dp_{i,i}=a_i\),其余为 \(-\infty\)。
显然有转移:
\(O(n^3)\) 转移即可。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e2+5;
int n,a[N],sum[N];
int dp[N][N];
signed main(){
	ios::sync_with_stdio(0);
	cin>>n;
	memset(dp,0xcf,sizeof(dp));
	for(int i=1;i<=n;i++)
		cin>>a[i],dp[i][i]=a[i];
	for(int i=2;i<=n;i++){
		for(int j=1;j+i-1<=n;j++){
			int s=j,e=j+i-1;
			for(int k=s;k<=e-1;k++)
				if(dp[s][k]==dp[k+1][e])
					dp[s][e]=max(dp[s][e],dp[s][k]+1);
		}
	}
	int ans=-1e9;
	for(int i=1;i<=n;i++)
		for(int j=i;j<=n;j++)
			ans=max(ans,dp[i][j]);
	cout<<ans;
	return 0;
}
T3
代码要去掉多测。
作业 T1
串串 + 区间 dp 简单题(?)。
令 \(dp_{i,j}\) 表示将区间 \([i,j]\) 涂成目标颜色所需的最少涂色次数。易得答案为 \(dp_{1,n}\)。
初始状态即为 \(dp_{i,i}=1\),其余为 \(\infty\)。
分类讨论:
- 
若 \(s_i=s_j\),则相当于在涂 \(i\) / \(j\) 时顺带将 \(j\) / \(i\) 涂上了,无需额外再涂一次,于是有转移 \(dp_{i,j}=\max(dp_{i+1,j},dp_{i,j-1})\)。 
- 
否则,有转移 \(dp_{i,j}=\max(dp_{i,j},dp_{i,k}+dp_{k+1,j})\)。 
\(O(n^3)\) 转移即可。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e2+5;
string s;
int n;
int dp[N][N];
signed main(){
	ios::sync_with_stdio(0);
	cin>>s,n=s.size(),s='#'+s;
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=n;i++) dp[i][i]=1;
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			if(s[i]==s[j])
				dp[i][j]=min(dp[i+1][j],dp[i][j-1]);
			else
				for(int k=i;k<j;k++)
					dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
		}
	}
	cout<<dp[1][n];
	return 0;
}
作业 T2
令 \(dp_{i,j}\) 表示区间(即一棵子树) \([i,j]\) 的最高加分。易得答案为 \(dp_{1,n}\)。
考虑枚举根 \(k\),则其左子树为 \([i,k]\),右子树为 \([k+1,j]\)。
于是有转移:
注意要特判 \(dp_{i+1,i}\) 与 \(dp_{i,i-1}\)(即只有左子树的节点 / 只有右子树的节点 / 叶子节点)。
至于输出前序遍历,我们每次更新答案时记录根,然后递归输出即可。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=31;
int n;
int dp[N][N];
int root[N][N];
void print(int l,int r){
	if(l>r) return;
	cout<<root[l][r]<<' ';
	if(l==r) return;
	print(l,root[l][r]-1);
	print(root[l][r]+1,r);
}
signed main(){
	ios::sync_with_stdio(0);
	cin>>n;
	memset(dp,0xcf,sizeof(dp));
	for(int i=1;i<=n;i++)
		cin>>dp[i][i],dp[i][i-1]=dp[i+1][i]=1,root[i][i]=i;
	for(int i=2;i<=n;i++){
		for(int j=1;j+i-1<=n;j++){
			int s=j,e=j+i-1;
			for(int k=s;k<=e;k++)
				if(dp[s][e]<dp[s][k-1]*dp[k+1][e]+dp[k][k])
					dp[s][e]=dp[s][k-1]*dp[k+1][e]+dp[k][k],
	 				root[s][e]=k;
		}
	}
	cout<<dp[1][n]<<'\n';
	print(1,n);
	return 0;
}
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号