「NOIP Record」#18 区间DP

区间形态的扩展

luogu3205 [HNOI2010] 合唱队

队列的形成方式是不断往左或往右扩展。

考虑区间 DP。发现对于一个区间 \([i,j]\),最后一个元素的来源会对转移产生影响,所以设 \(f(i,j,0/1)\) 为考虑区间 \([i,j]\),其中最后一个元素是从左边还是右边加入的方案数。

转移很简单 \[ f(i,j,0) = [a_i < a_{i+1}]f(i+1,j,0) + [a_i < a_j] f(i+1,j,0) \]

\[ f(i,j,1) = [a_j > a_{i}] f(i,j-1,0) + [a_j > a_{j-1}] f(i,j-1,1) \]

注意对于 \([i,i]\),钦定从一边转移过来即可。

// Problem: P3205 [HNOI2010] 合唱队
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3205
// Author: yozora0908
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=1005, mod=19650827;
int n, a[N], f[N][N][2];
signed main() {
	n=read();
	rep(i,1,n) a[i]=read(),  f[i][i][0]=1;
	rep(l,2,n) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		if(a[i]<a[i+1]) (f[i][j][0]+=f[i+1][j][0])%=mod;
		if(a[i]<a[j]) (f[i][j][0]+=f[i+1][j][1])%=mod;
		if(a[j]>a[i]) (f[i][j][1]+=f[i][j-1][0])%=mod;
		if(a[j]>a[j-1]) (f[i][j][1]+=f[i][j-1][1])%=mod;
	}
	printf("%lld\n",(f[1][n][0]+f[1][n][1])%mod);
	return 0;
}

某道题

你有 \([1,n]\) 中的正整数各 \(2\) 个,存在 \(k\) 个限制。

要求将这 \(2n\) 个数排成一个序列,满足以下两个条件:

  • 如果存在一个最大的数所在的的位置 \(p\),满足 \(p>1\),那么 \([1,p]\) 中的数单调不降。
  • 如果存在一个最大的数所在的的位置 \(p\),满足 \(p<n\),那么 \([p,n]\) 中的数单调不增。

每个限制条件形如x op y

其中op可能为<<==>=>,表示下标为 \(x\) 的数与下标为 \(y\) 的数必须满足的大小关系。

求方案数,对 \(998244353\) 取模。

\(n \le 1000\)\(0 \le k \le 3 \times 10^4\)

注意:原题没有取模,\(n \le 500\)。但是复杂度的瓶颈在于高精度,所以这里加上取模,并且增大数据范围。

先不考虑限制。

如何生成这样的序列呢?考虑从大到小放。两个 \(n\) 肯定要放到中间,剩下的数要么两边放一个,要么都放在一边。这样就是呈区间形态的扩展方式,考虑区间 DP。

\(f(i,j,k)\) 为考虑了 \([k,n]\) 中的数,放完了区间 \([i,j]\) 的方案。

仔细思考能发现记录 \(k\) 根本没有用,只要区间扩展时每次只扩展 \(2\) 的长度,就一定合法。

\(f(i,j)\) 为放完了区间 \([i,j]\) 的方案数。

如果没有限制的话,能发现答案就是 \(3^{n-1}\)

考虑限制。

我们讨论三种方法是否会受限制。

  1. 放的两个位置必须都允许相等。
  2. 这两个位置都必须严格小于其他位置。

暴力判断就是 \(O(n)\) 的。

利用大小关系的对称性。设 \(s(i,j)\)\(i\)\(j\) 的限制,没有就是 \(0\)<\(1\)\(\le\)\(2\)=\(3\)。对 \(j\) 这一维求前缀和,就能 \(O(1)\) 判断一个区间对一个位置的限制,加以讨论即可做到 \(O(n^2)\)

下面的代码是带着高精的。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=1005;
string st;
int n, k, p[N][N], s[N][N];
struct Int {
	short a[245], deg=0;
	void reset() { SET(a,0); deg=0; }
	bool zero() { return !deg||(deg==1&&a[0]==0); }
	void operator=(int x) { deg=1; a[deg-1]=x; }
	void operator+=(Int& x) {
		deg=max(deg,x.deg);
		for(int i=0;i<deg;++i) {
			a[i]+=x.a[i];
			a[i+1]+=a[i]/10;
			a[i]%=10;
		}
		while(a[deg]) ++deg;
	}
	void print() {
		for(int i=deg-1;~i;--i) printf("%d",a[i]);
	}
} f[N][N];

signed main() {
	n=read()*2, k=read();
	rep(i,1,k) {
		int l=read();
		cin>>st;
		int r=read();
		if(st=="<") p[l][r]=1, p[r][l]=5;
		else if(st=="<=") p[l][r]=2, p[r][l]=4;
		else if(st=="=") p[l][r]=p[r][l]=3;
		else if(st==">=") p[l][r]=4, p[r][l]=2;
		else p[l][r]=5, p[r][l]=1;
	}
	rep(i,1,n) rep(j,1,n) s[i][j]=s[i][j-1]+(p[i][j]>2);
	f[1][n]=0;
	for(int i=1;i<n;++i) if(!p[i][i+1]||(p[i][i+1]>=2&&p[i][i+1]<=4)) f[i][i+1]=1;
	for(int l=2;l<=n;l+=2) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		if(!f[i][j].zero()) continue;
		if(p[i][i+1]!=1&&p[i][i+1]!=5&&!f[i+2][j].zero()) {
			if(s[i][j]==s[i][i+1]&&s[i+1][j]==s[i+1][i+1]) f[i][j]+=f[i+2][j];
		}
		if(p[j-1][j]!=1&&p[j-1][j]!=5&&!f[i][j-2].zero()) {
			if(s[j][j-2]==s[j][i-1]&&s[j-1][j-2]==s[j-1][i-1]) f[i][j]+=f[i][j-2];
		}
		if(p[i][j]!=1&&p[i][j]!=5&&!f[i+1][j-1].zero()) {
			if(s[i][j-1]==s[i][i]&&s[j][j-1]==s[j][i]) f[i][j]+=f[i+1][j-1];
		}
	}
	f[1][n].print();
	return 0;
}

关于这个,

求出区间信息后贪心划分

UVA1437 String painter

我断言一定存在一种最优的方案满足对于任意两次染色:它们的区间要么不交,要么靠后的那次被靠前的那次包含并且不共端点。

By FZzzz

先考虑空串怎么做。

\(f(i,j)\) 为完成区间 \([i,j]\) 的涂色的最小代价。

  • 如果 \(S[i]=S[j]\),那么 \(f(i,j) = f(i,j-1)\)
  • 否则一定一个断点,满足左右两边的染色区间不相交。枚举断点即可。

考虑原问题,不难发现最优解一定是若干个不相交区间的 \(f\) 拼接成的。

\(g(i)\) 为完成 \([1,i]\) 的涂色的最小代价。如果 \(A[i] = B[i]\),那么直接继承 \(g(i-1)\)。否则枚举最后一段的划分点,求 \(\min_{j=1}^{i-1} \Big\{g(j)+f(j+1,i) \Big\}\)

luogu2470 [SCOI2007] 压缩

和上面那题的套路很像。

先考虑没有M的情况。

发现直接做是不行的,比如aaaabaaaab,最短的压缩应该是aRRbR。也就是说,这里有一个子问题的结构。

\(g(i,j)\) 为不考虑M,区间 \([i,j]\) 的最小压缩长度。

仔细思考,发现这个区间能折半的话就折半,否则就只能拖在后面。 \[ g(i,j) = \min \begin{cases} g(i,i+mid-1)+1 & \text{if } \; valid(i,j) \\ g(i,j-1)+1 \end{cases} \] 其中 \(valid(i,j)\) 表示 \([i,j]\) 能否从中间划分成两个相同的串。

然后设 \(f(i)\)\([1,i]\) 的最小划分,有 \[ f(i) = \min_{j=0}^{i-1} \Big\{ f(j) + g(j+1,i) \Big\} \] 如果用哈希求 \(valid(i,j)\),那么复杂度就是 \(O(n^2)\),优于洛谷题解区所有题解。

但是数据太水,如果用哈希可能因为常数大跑得更慢,所以本人采用了暴力做法,是 \(O(n^3)\) 的。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=55;
int n, f[N], g[N][N];
string s;
signed main() {
	cin>>s;
	n=s.size();
	s=" "+s;
	rep(i,1,n) g[i][i]=1, g[i][i+1]=2;
	for(int lim=3;lim<=n;++lim) for(int i=1;i+lim-1<=n;++i) {
		int j=i+lim-1;
		int& res=g[i][j];
		res=lim;
		if(lim%2==0&&s.substr(i,lim/2)==s.substr(i+lim/2,lim/2)) res=min(res,g[i][i+lim/2-1]+1);
		res=min(res,g[i][j-1]+1);
	}
	rep(i,1,n) f[i]=g[1][i];
	for(int i=2;i<=n;++i) {
		for(int j=1;j<i;++j) {
			f[i]=min(f[i],f[j]+g[j+1][i]+1);
		}
	}
	printf("%d\n",f[n]);
	return 0;
}

类题还有CF1312E Array Shrinking

对相交区间的处理

luogu5851 [USACO19DEC] Greedy Pie Eaters

一个合法的顺序,一定若干个区间不断向外扩展。

一次收益只需要保证一个元素的存在,可以就此入手。

区间重叠难以处理,而数据范围较小,可以对区间范围进行限制。

\(g(k,i,j)\) 为只考虑被 \([i,j]\) 完全包含的区间,所有覆盖过 \(k\) 的区间的最大权值。

容易在 \(O(n^3)\) 的时间里处理出来。

\(f(i,j)\) 为只考虑被 \([i,j]\) 完全包含的区间,能够产生的最大收益。

枚举最后一个被干掉的元素 \(k\),有转移 \[ f(i,j) = \max_{k=i}^j \Big\{ f(i,k-1) + g(k,i,j) + f(k+1,j) \Big\} \] 对于区间 \([i,j]\),一定存在最后一个被干掉的元素。想要这个元素此时依然存在,之前选择的区间就不能包含它,而两边都是子问题。

// Problem: P5851 [USACO19DEC] Greedy Pie Eaters P
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5851
// Author: yozora0908
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=305;
int n, m, f[N][N], g[N][N][N];
signed main() {
	n=read(), m=read();
	rep(i,1,m) {
		int w=read(), l=read(), r=read();
		rep(k,l,r) g[k][l][r]=max(g[k][l][r],w);
	}
	for(int l=1;l<=n;++l) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		rep(k,i,j) {
			g[k][i][j]=max({g[k][i][j],g[k][i+1][j],g[k][i][j-1]});
		}
	}
	rep(i,1,n) f[i][i]=max(f[i][i],g[i][i][i]);
	for(int l=2;l<=n;++l) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		rep(k,i,j) {
			f[i][j]=max(f[i][j],f[i][k-1]+g[k][i][j]+f[k+1][j]);
		}
	}
	printf("%lld\n",f[1][n]);
	return 0;
}

LOJ#2688. 「POI2015」洗车

\(f(i,j,k)\) 为考虑洗车店区间 \([i,j]\) 和被区间 \([i,j]\) 完全包含的消费者区间,\([i,j]\) 的最小值为 \(k\) 时,能得到的最大收益。

先将 \(c_i\) 离散化了。

我们把贡献放到点上,求出 \(g(p,k)\) 表示在当前区间内,覆盖了 \(p\)\(c_i \ge k\) 的区间数量。有转移 \[ f(i,j,k) = \max_{p=i}^j \Big\{ f(i,p-1,k) + g(p,k) \times val(k) + f(p+1,j,k) \Big\} \] 正确性就比较显然了。

输出方案只需要记录最优决策点和对应决策值。

duyi的博客中说这是笛卡尔树 DP。

这个说法我无法否认,因为这就是自底向上重现最值分治的过程。

但是也完全可以当作区间 DP 来思考。

// Problem: P3592 [POI2015] MYJ
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3592
// Author: yozora0908
// Memory Limit: 250 MB
// Time Limit: 2000 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=55, M=4005;
int n, m, cnt, a[M], b[M], c[M], t[M];
int f[N][N][M], g[N][M], lst[N][N][M], v[N][N][M], ans[N];
void lsh() {
	sort(t+1,t+m+1);
	cnt=unique(t+1,t+m+1)-(t+1);
	rep(i,1,m) c[i]=lower_bound(t+1,t+cnt+1,c[i])-t;	
}
void print(int i,int j,int k) {
	if(i>j) return;
	ans[lst[i][j][k]]=t[v[i][j][k]];
	print(i,lst[i][j][k]-1,v[i][j][k]);
	print(lst[i][j][k]+1,j,v[i][j][k]);
}
signed main() {
	n=read(), m=read();
	rep(i,1,m) {
		a[i]=read(), b[i]=read(), c[i]=t[i]=read();
	}
	lsh();
	for(int l=1;l<=n;++l) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		SET(g,0);
		rep(k,1,m) if(i<=a[k]&&b[k]<=j) {
			rep(p,a[k],b[k]) ++g[p][c[k]];
		}
		rep(p,i,j) per(k,cnt,1) g[p][k]+=g[p][k+1];
		lst[i][j][cnt+1]=i, v[i][j][cnt+1]=cnt;
		per(k,cnt,1) {
			f[i][j][k]=f[i][j][k+1];
			lst[i][j][k]=lst[i][j][k+1];
			v[i][j][k]=v[i][j][k+1];
			rep(p,i,j) if(f[i][p-1][k]+f[p+1][j][k]+g[p][k]*t[k]>f[i][j][k]) {
				f[i][j][k]=f[i][p-1][k]+f[p+1][j][k]+g[p][k]*t[k];
				lst[i][j][k]=p, v[i][j][k]=k;
			}
		}
	}
	printf("%lld\n",f[1][n][1]);
	print(1,n,1);
	rep(i,1,n) printf("%lld ",ans[i]);
	return 0;
}

另外有类题luogu4766 Outer space invaders

区间左右取数

这个就相对固定了。

ABC303G Bags Game

一类在区间左右选物品的博弈问题。

\(f(i,j)\) 为只考虑区间 \([i,j]\),先手能得到的最大收益。

第一类操作,转移显然。

考虑第二三类操作。

以第二类操作为例,设 \(\beta=\min(B,len)\),枚举 \(x+y=\beta\),有转移 \[ f(i,j) \leftarrow \max_{x=0}^{\beta} \Big\{ \sum_{k=i}^{i+x-1} a_i + \sum_{j-y+1}^{j} a_i - f (i+x,j-y) \Big\} \] 式子变形一下就是 \[ \max_{x=0}^{\beta} \Bigg\{ sum(i,j) - \Big( f (i+x,j-y) + sum(i+x,j-y) \Big) \Bigg\} \] 考虑优化。

能发现对于一个固定的 \(len=j-i+1\),所有决策区间的长度都是 \(j-y-(i+x)+1 = len-\beta\)

\(lb=len-\beta\)

当从 \([i-1,j-1]\)\([i,j]\) 时,我们只会丢掉决策 \(f(i-1,i+lb-2)\),增加决策 \(f(j-lb+1,j)\)

我们只需要用单调队列维护中间那个东西的最小值就行了。

实现细节颇多,具体看代码。

// Problem: [ABC303G] Bags Game
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/AT_abc303_g
// Author: yozora0908
// Memory Limit: 1 MB
// Time Limit: 2500 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=3005;
int n, A, B, C, D, a[N], s[N], f[N][N];
int q1[N], q2[N];
int sum(int i,int j) { return s[j]-s[i-1]; }
signed main() {
	n=read(), A=read(), B=read(), C=read(), D=read();
	
	rep(i,1,n) {
		a[i]=read();
		s[i]=s[i-1]+a[i];
		f[i][i]=a[i];
	}
	for(int len=2;len<=n;++len) {
		int l1=1, r1=0;
		int l2=1, r2=0;
		int lb=len-B, ld=len-D;
		f[1][len]=max(a[1]-f[2][len],a[len]-f[1][len-1]);
		
		if(lb>0) {

			for(int i=1;i+lb-1<=len;++i) {
             	int ll=q1[r1], rr=q1[r1]+lb-1;
             	// 当前加入的区间是[i,i+lb-1]
				while(l1<=r1&&f[ll][rr]+sum(ll,rr)>=f[i][i+lb-1]+sum(i,i+lb-1)) {
					--r1;
					ll=q1[r1], rr=q1[r1]+lb-1;
				}
				q1[++r1]=i;
			}
            // 先把[1,len]中所有决策都扔进,注意边界
			int ll=q1[l1], rr=q1[l1]+lb-1;
			f[1][len]=max(f[1][len],sum(1,len)-A-sum(ll,rr)-f[ll][rr]);
		} else f[1][len]=max(f[1][len],sum(1,len)-A);
		
		if(ld>0) {

			for(int i=1;i+ld-1<=len;++i) {
				int ll=q2[r2], rr=q2[r2]+ld-1;
				while(l2<=r2&&f[ll][rr]+sum(ll,rr)>=f[i][i+ld-1]+sum(i,i+ld-1)) {
					--r2;
					ll=q2[r2], rr=q2[r2]+ld-1;
				}
				q2[++r2]=i;
			}
			
			int ll=q2[l2], rr=q2[l2]+ld-1;
			f[1][len]=max(f[1][len],sum(1,len)-C-sum(ll,rr)-f[ll][rr]);
			
		} else f[1][len]=max(f[1][len],sum(1,len)-C);
		
		
		for(int i=2;i+len-1<=n;++i) {
			int j=i+len-1;
			f[i][j]=max(a[i]-f[i+1][j],a[j]-f[i][j-1]);
			if(lb<=0) f[i][j]=max(f[i][j],sum(i,j)-A);
			else {
				while(l1<=r1&&q1[l1]<i) ++l1;
				int ll=q1[r1], rr=q1[r1]+lb-1;
				while(l1<=r1&&f[ll][rr]+sum(ll,rr)>=f[j-lb+1][j]+sum(j-lb+1,j)) {
					--r1;
					ll=q1[r1], rr=q1[r1]+lb-1;
				}
				q1[++r1]=j-lb+1;
				ll=q1[l1], rr=q1[l1]+lb-1;
                // 注意顺序,我们刚才加入的是当前区间所带来的新决策,所以更新必须放到最后
				f[i][j]=max(f[i][j],sum(i,j)-A-sum(ll,rr)-f[ll][rr]);
				
				
			}
			if(ld<=0) f[i][j]=max(f[i][j],sum(i,j)-C);
			else {
				while(l2<=r2&&q2[l2]<i) ++l2;
				int ll=q2[r2], rr=q2[r2]+ld-1;
				while(l2<=r2&&f[ll][rr]+sum(ll,rr)>=f[j-ld+1][j]+sum(j-ld+1,j)) {
					--r2;
					ll=q2[r2], rr=q2[r2]+ld-1;
				}
				q2[++r2]=j-ld+1;
				ll=q2[l2], rr=q2[l2]+ld-1;
				f[i][j]=max(f[i][j],sum(i,j)-C-sum(ll,rr)-f[ll][rr]);
				
				
			}
		}
	}
	printf("%lld\n",f[1][n]);
	return 0;
}

杂题

CF149D Coloring Brackets

先用栈求出点 \(i\) 的匹配点 \(\text{match}(i)\)

\(f(i,j,a,b)\) 为考虑区间 \([i,j]\),其中左端点的染色情况和为 \(a\),右端点的染色情况为 \(b\) 的方案数。

分如下情况讨论。

  • \(\text{match}(i)=j\)

    如果 \(i+1=j\),那么直接讨论就好了。否则枚举颜色,用 \(f(i+1,j-1)\) 转移。

  • \(\text{match}(i) \neq j\)。我们考虑把 \([i,\text{match}(i)]\) 拼接到 \([\text{match}(i)+1,j]\) 上。枚举颜色讨论即可。

  • 注意由于 \(\text{match}(i)\) 不一定在 \(i\) 的右边,所以要判断所有 \(i>j\) 的情况。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=705, mod=1e9+7;
int n, match[N], f[N][N][3][3];
int tp, st[N];
char s[N];
void dfs(int l,int r) {
	if(l>r) return;
	if(l+1==r) {
		f[l][r][0][1]=f[l][r][0][2]=f[l][r][1][0]=f[l][r][2][0]=1;
		return;
	}
	if(match[l]==r) {
		dfs(l+1,r-1);
		rep(i,0,2) rep(j,0,2) {
			if(j!=1) (f[l][r][0][1]+=f[l+1][r-1][i][j])%=mod;
			if(j!=2) (f[l][r][0][2]+=f[l+1][r-1][i][j])%=mod;
			if(i!=1) (f[l][r][1][0]+=f[l+1][r-1][i][j])%=mod;
			if(i!=2) (f[l][r][2][0]+=f[l+1][r-1][i][j])%=mod;
		}		
	} else {
		dfs(l,match[l]);
		dfs(match[l]+1,r);
		rep(i,0,2) rep(j,0,2) rep(p,0,2) rep(q,0,2) {
			if(j==p&&j) continue;
			if(!i||!p) (f[l][r][i][q]+=f[l][match[l]][i][p]*f[match[l]+1][r][j][q]%mod)%=mod;
            // 相邻不能相等,匹配的两边不能都染色
		}
	}
}
signed main() {
	scanf("%s",s+1);
	n=strlen(s+1);
	rep(i,1,n) {
		if(s[i]=='(') st[++tp]=i;
		else {
			match[i]=st[tp];
			match[st[tp]]=i;
			--tp;
		}
	}
	dfs(1,n);
	int ans=0;
	rep(i,0,2) rep(j,0,2)  (ans+=f[1][n][i][j])%=mod;
	printf("%lld\n",ans);
	return 0;
}

luogu3147 [USACO16OPEN] 262144

先考虑 \(O(n^3)\) 怎么做。

这类整个区间合并成一个数的题都有一个套路:只考虑整个区间合并成一个数的情况。

\(f(i,j)\) 为区间 \([i,j]\) 能合成的数的最大值,转移枚举断点,再判断两边是否相等即可。

然后这题正解就和 DP 没关系了。

\(f(i,j)\) 为从 \(j\) 开始合并一串数得到 \(i\),这串数的个数。 \[ f(i,j) = f(i-1,j) + f\Big(i-1,j+f(i-1,j)\Big) \] 类题是 luogu4805 [CCC2016] 合并饭团,不过那题的主要部分是单调性优化,这里就不展开了。

luogu2890 [USACO07OPEN] Cheapest Palindrome

\(f(i,j)\) 为把区间 \([i,j]\) 搞成回文的最小代价。

如果 \(S[i]=S[j]\),那么直接继承 \(f(i+1,j-1)\)

否则就是 \(\min\Big( f(i+1,j)+cost_i,f(i,j-1)+cost_j \Big)\)

其中 \(cost_i\) 表示增加或删去 \(S[i]\) 的较小代价。

对于区间 \([i,j]\),如果 \(S[i] \neq S[j]\),那么要想形成回文串,总会拿出 \(S[i],S[j]\) 其中之一,删掉或添加一个一样的在另一边,剩下的部分也要形成回文串,这是一个子问题。

// Problem: P2890 [USACO07OPEN] Cheapest Palindrome G
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2890
// Author: yozora0908
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=2005;
int n, m, f[N][N], c[100][2];
char s[N], o[5];
signed main() {
	n=read(), m=read();
	scanf("%s",s+1);
	
	rep(i,1,n) {
		scanf("%s",o);
		c[o[0]-'a'][0]=read(), c[o[0]-'a'][1]=read();
	}
	SET(f,0x3f);
	rep(i,1,m) f[i][i]=0;
	for(int l=2;l<=m;++l) for(int i=1;i+l-1<=m;++i) {
		int j=i+l-1;
		if(s[i]==s[j]) {
			if(i+1==j) f[i][j]=0;
			else f[i][j]=min(f[i][j],f[i+1][j-1]);
		}
		f[i][j]=min({f[i][j],f[i+1][j]+min(c[s[i]-'a'][0],c[s[i]-'a'][1]),f[i][j-1]+min(c[s[j]-'a'][0],c[s[j]-'a'][1])});
	}
	printf("%lld\n",f[1][m]);
	return 0;
}

LOJ#2063. 「HAOI2016」字符合并

这里用 \(m\) 代替题目中的 \(k\)

注意到任何区间合并后都一定是一个 \(m\) 位二进制数,而 \(m\) 很小,可以考虑状压。

\(f(i,j,S)\) 为区间 \([i,j]\) 合并为 \(S\) 的最大收益。

考虑如何转移。先讨论如何得到 \(S\),再讨论合并成什么。

可以枚举 \(S\) 的最后一位是哪个区间合并而来,复杂度是 \(O(\frac{n}{m})\) 的。

合并只能将整个串合并为 \(0\)\(1\),不难发现这样做的充要条件是 \((m-1) \mid (len-1)\)

枚举 \(S\),对于转化成 \(0\)\(1\) 分别取最大值,最后更新 \(f(i,j,0/1)\) 即可。

时间复杂度是 \(O(n^2 2^m\frac{n}{m})\),勉强通过。

// Problem: #2063. 「HAOI2016」字符合并
// Contest: LibreOJ
// URL: https://loj.ac/p/2063
// author: yozora0908
// Memory Limit: 256 MB
// Time Limit: 1000 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=305, inf=0x3f3f3f3f;
int n, m, U, to[1<<8], w[1<<8], f[N][N][1<<8];
char s[N];
signed main() {
	n=read(), m=read();
	U=(1<<m)-1;
	SET(f,-0x3f);
	scanf("%s",s+1);
	rep(i,1,n) f[i][i][s[i]-'0']=0;
	for(int S=0;S<=U;++S) to[S]=read(), w[S]=read();
	for(int l=2;l<=n;++l) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		for(int S=0;S<=U;++S) {
			for(int k=j-1;k>=i;k-=m-1) {
				f[i][j][S]=max(f[i][j][S],f[i][k][S>>1]+f[k+1][j][S&1]);
                // 注意枚举顺序
			}
		}
		if((l-1)%(m-1)==0) {
			int res0=0, res1=0;
			for(int S=0;S<=U;++S) {
				if(to[S]==0) res0=max(res0,f[i][j][S]+w[S]);
				else res1=max(res1,f[i][j][S]+w[S]);
			}
			f[i][j][0]=max(f[i][j][0],res0), f[i][j][1]=max(f[i][j][1],res1);
		}
	}
	int ans=0;
	for(int S=0;S<=U;++S) ans=max(ans,f[1][n][S]);
	printf("%lld\n",ans);
	return 0;
}

LOJ#2292. 「THUSC 2016」成绩单

注意到 \(n\) 只有 \(50\),可以先将 \(W_i\) 离散化了。

注意到分发顺序不重要,我们钦定任何区间都是从左往右分发。

\(g(i,j,mx,mn)\) 表示把 \([i,j]\) 取到还剩下一段值域为 \([mn,mx]\) 的区间的最小代价,\(f(i,j)\) 表示取完 \([i,j]\) 的最小代价。

首先我们可以让 \(j\) 加入 \(g(i,j-1,mx,mn)\),注意 \(W_j\)\(mx\)\(mn\) 的影响。

否则我们枚举和 \(j\) 一起消掉的一段。 \[ g(i,j,mx,mn) = \min_{k=i}^{j-1} \Big\{ g(i,k,mx,mn) + f(k+1,j) \Big\} \]

// Problem: P5336 [THUSC2016] 成绩单
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5336
// Author: yozora0908
// Memory Limit: 500 MB
// Time Limit: 2000 ms
// 
// Let's Daze
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=55;
int n, m, a, b, s[N], f[N][N], g[N][N][N][N], t[N];
void lsh() {
	sort(t+1,t+m+1);
	m=unique(t+1,t+m+1)-(t+1);
	rep(i,1,n) s[i]=lower_bound(t+1,t+m+1,s[i])-t;
}
signed main() {
	n=read(), a=read(), b=read();
	rep(i,1,n) s[i]=read(), t[++m]=s[i];
	lsh();
	SET(f,0x3f), SET(g,0x3f);
	rep(i,1,n) {
		f[i][i]=a;
		g[i][i][s[i]][s[i]]=0;
	}
	for(int l=2;l<=n;++l) for(int i=1;i+l-1<=n;++i) {
		int j=i+l-1;
		 rep(mx,1,m) rep(mn,1,mx) {
		 	int& x=g[i][j][max(mx,s[j])][min(mn,s[j])];
		 	x=min(x,g[i][j-1][mx][mn]);
		 	rep(k,i,j-1) g[i][j][mx][mn]=min(g[i][j][mx][mn],g[i][k][mx][mn]+f[k+1][j]);
		 }
		rep(mx,1,m) rep(mn,1,mx) {
			f[i][j]=min(f[i][j],g[i][j][mx][mn]+a+b*(t[mx]-t[mn])*(t[mx]-t[mn]));
		}
	}
	printf("%lld\n",f[1][n]);
	return 0;
}

LOJ#6092. 「Codeforces Round #418」恋爱循环

严格来说算是区间 DP 吗?

对于每个字符,把所有点提取成连续的,保留各自的下标用作距离。我们断言,最终的修改一定指挥让某一个区间的这个字符连成一片。

\(f_{\pi}(i,j)\) 为考虑字符 \(\pi\),把区间 \([i,j]\) 里的 \(\pi\),通过修改中间字符的方式搞成连续的所需要的最小代价。

那么每个询问等价于求一个区间 \([l,r]\),满足 \(f_c(l,r) \le m\) 并且 \(\min\Big(n,r-l+1+m \Big)\) 最大。

不难发现对于不断递增的 \(r\)\(l\) 是有决策单调性的,直接双指针即可。

然而还是无法通过,考虑预处理答案。

能发现总复杂度是 \(O\Big(\sum_{i=1}^{26} cnt_i^2 + q\Big)\),其中 \(cnt_i\) 表示第 \(i\) 个字符的数量。

后面那个东西在所有字符都相同时有上界,可以通过。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=1505;
int n, q, a[N], sum[30][N], ans[30][N];
vector<int> v[30];
vector<vector<int> > f[30];
char s[N], cc[2];
void dp(int x) {
	f[x].resize(v[x].size()+3);
	for(int i=0;i<v[x].size();++i) f[x][i].resize(v[x].size()+3);
	for(int i=0;i<v[x].size();++i) f[x][i][i]=0;
	for(int l=2;l<=v[x].size();++l) for(int i=0;i+l-1<v[x].size();++i) {
		int j=i+l-1;
		f[x][i][j]=f[x][i][j-1]+v[x][j]-v[x][j-1]-1;
	}
}
void solve(int x) {
	for(int i=1;i<=n;++i) {
		int l=0, r=0;
		while(r+1<v[x].size()&&f[x][l][r+1]<=i) ++r;
		ans[x][i]=max(ans[x][i],min(r-l+1+i,n));
		for(;r<v[x].size();++r) {
			while(l<=r&&f[x][l][r]>i) ++l;
			if(l<=r) ans[x][i]=max(ans[x][i],min(r-l+1+i,n));
		}
	}
}
void prework() {
	rep(i,1,26) if(v[i].size()) dp(i);
	rep(i,1,26) if(v[i].size()) solve(i);
}
signed main() {
	n=read();
	scanf("%s",s+1);
	rep(i,1,n) {
		a[i]=s[i]-'a'+1;
		v[a[i]].pb(i);
	}
	prework();
	q=read();
	while(q--) {
		int m=read();
		scanf("%s",cc);
		int c=cc[0]-'a'+1;
		if(v[c].size()) printf("%lld\n",ans[c][m]); else printf("%lld\n",m);
	}
	return 0;
}

「NOIP Record」#18 区间DP
https://yozora0908.github.io/2023/noip-record-18/
作者
yozora0908
发布于
2023年8月30日
许可协议