dp专题训练

Frog 1

我们设 \(f_i\) 表示跳到第 \(i\) 个石头的最小总费用。于是我们可以推出转移方程:

\(f_i=\min(f_{i-1}+|h_{i-1}-h_i|,f_{i-2}+|h_{i-2}-h_i|)\)

当然这个方程在转移的时候不能越界。

于是做一个线性 \(dp\) 即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,h[N],f[N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>h[i];
	}
	memset(f,0x3f,sizeof f);
	f[1]=0;
	for(int i=1;i<=n;i++){
		if(i>1){
			f[i]=min(f[i],f[i-1]+abs(h[i]-h[i-1]));
		}
		if(i>2){
			f[i]=min(f[i],f[i-2]+abs(h[i]-h[i-2]));
		}
	}
	cout<<f[n];
	return 0;
}

Frog 2

我们设 \(f_i\) 表示跳到第 \(i\) 个石头的最小总费用。于是我们可以推出转移方程:

\(\displaystyle f_i=\min_{j=\max(1,i-k)}^{j<i}(f_j+|h_i-h_j|)\)

于是做一个线性 \(dp\) 即可,其实上一题就是 \(k=2\) 的特殊情况。

这里给个思考题:

  • \(k=n\) 怎么做?

显然是好做的,答案就是 \(h_1-h_n\)。这个不难思考。因为如果中间跳到中转点,答案一定不会更优。所以不中转一定不劣。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,k,h[N],f[N];
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>h[i];
	}
	memset(f,0x3f,sizeof f);
	f[1]=0;
	for(int i=1;i<=n;i++){
		for(int j=max(1ll,i-k);j<i;j++){
			f[i]=min(f[i],f[j]+abs(h[i]-h[j]));
		}
	}
	cout<<f[n];
	return 0;
}

Vacation

首先我们按照原来的想法设 \(f_i\) 为第 \(i\) 天做完活动后的的最大幸福值。然后,就发现根本没法转移,因为题中的两天不能进行同一种活动的限制我们没有使用。我们考虑缺了这样一个东西,就把它加到状态里。

\(f{i,j}\) 表示第 \(i\) 天做的是第 \(j\) 个活动,能获得的最大幸福值,这里 \(j=0/1/2\)

转移方程非常好推:\(\displaystyle f_{i,j}=\max_{j}^{j\ne i}(f_{i-1,j}+val_j)\)\(val_j\) 为第 \(j\) 种活动方式获得的幸福度。

提示 \(1\):设状态后发现这个状态出现了问题,可以把出现问题的这个东西加到状态里。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,a[N],b[N],c[N],f[N][3];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i]>>c[i];
	}
	f[1][0]=a[1];
	f[1][1]=b[1];
	f[1][2]=c[1];
	for(int i=2;i<=n;i++){
		f[i][0]=max(f[i-1][1],f[i-1][2])+a[i];
		f[i][1]=max(f[i-1][0],f[i-1][2])+b[i];
		f[i][2]=max(f[i-1][0],f[i-1][1])+c[i];
	}
	cout<<max({f[n][0],f[n][1],f[n][2]});
	return 0;
}

Knapsack 1

我们考虑设 \(f_{i,j}\) 为考虑前 \(i\) 个物品,背包容量为 \(j\) 能获得的最大价值。那么这个方程非常好推:

\(\displaystyle f_{i,j}=\max_{j}^{j\ge w_i}(f_{i-1,j-w_i}+v_i)\)

到这里已经可以通过了,但是我们还能做的更好。

可以发现 \(i\) 在转移时只会用到 \(i-1\)。于是我们第一维不需要开 \(n\) 的大小,只需要开 \(2\) 的大小即可。

提示 \(2\):在空间比较紧张且 \(i\) 的转移只需要用到 \(i-1\) 时,可以采用滚动数组优化,把其中一维的空间变为常数级别。

事实上可以继续优化。我们可以把第一维直接优化掉,我们在循环 \(j\) 时进行倒序循环,这样就可以在线性空间内解决 \(01\) 背包问题。

补充一句,完全背包也可以这样做,但是应用完全背包时 \(j\) 需要正序循环。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 105
#define M 100005
using namespace std;
int n,m,w[N],v[N],f[M];
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>v[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){
			f[j]=max(f[j],f[j-w[i]]+v[i]);
		}
	}
	cout<<f[m];
	return 0;
}

Knapsack 2

我们考虑设 \(f_{i,j}\) 为考虑前 \(i\) 个物品,背包容量为 \(j\) 能获得的最大价值。然后就发现,空间上不能接受,即使优化掉第一维。

但是,可以发现 \(v_i\) 非常的小,那么,能不能转换一下状态,设 \(f_{i,j}\) 为前 \(i\) 个物品,获得的价值为 \(j\) 所需的最小背包容量。于是有两个转移方程:

  • \(f_{i,j}=\min(f_{i,j},f_{i-1,j})\)

  • \(\displaystyle f_{i,j}=\min_{j}^{j\ge v_i}(f_{i,j},f{i-1,j-v_i}+w_i)\)

答案非常好求,就是找到最大的 \(i\),使得 \(f_{n,i}\le m\),答案就是这个 \(i\)

提示 \(3\):如果一个状态会炸空间并且无法优化且此时答案的值域很小,可以将答案作为状态,状态作为答案。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 105
#define M 100005
using namespace std;
int n,m,w[N],v[N],f[N][M];
signed main(){
	cin>>n>>m;
	int sum=0;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>v[i];
		sum+=v[i];
	}
	memset(f,0x3f,sizeof f);
	f[0][0]=0;
	for(int i=1;i<=n;i++){
		for(int j=sum;j>=0;j--){
			f[i][j]=min(f[i][j],f[i-1][j]);
			if(j>=v[i])f[i][j]=min(f[i][j],f[i-1][j-v[i]]+w[i]);
		}
	}
	int res=0;
	for(int i=sum;i>=0;i--){
		if(f[n][i]<=m){
			res=i;
			break;
		}
	}
	cout<<res;
	return 0;
}

LCS

最长公共子序列的状态十分经典。设 \(f_{i,j}\)\(s\) 的前 \(i\) 个字符和 \(t\) 的前 \(j\) 个字符的最长公共子序列的长度。转移方程:

\(f_{i,j}=\max(f_{i-1,j},f_{i,j-1})\)

\(s_i\)\(t_j\) 相同时,\(f_{i,j}=\max(f_{i,j},f_{i-1,j-1}+1)\)

这样我们就可以求出整个序列的最长公共子序列的长度了。我们可以从这个倒推出方案。

分三种情况讨论:

  • \(s_i\)\(t_j\) 相同且 \(f_{i,j}=f_{i-1,j-1}+1\):那么答案的第 \(f_{i,j}\) 位就是 \(s_i\)。然后 \(i=i-1,j=j-1\)

  • \(f_{i,j}\)\(f_{i-1,j}\) 相同:\(i=i-1\)

  • \(f_{i,j}\)\(f_{i,j-1}\) 相同:\(j=j-1\)

这里事实上就是枚举 \(f_{i,j}\) 是由 \(3\) 种转移方式的哪一个转移而来的,倒推回去,其实拿个数组记录也可以,就是有点麻烦。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 3005
using namespace std;
int n,m,f[N][N];
char s[N],t[N],res[N];
signed main(){
	cin>>s+1>>t+1;
	n=strlen(s+1);
	m=strlen(t+1);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=max({f[i-1][j],f[i][j-1]});
			if(s[i]==t[j])f[i][j]=max(f[i][j],f[i-1][j-1]+1);
		}
	}
	int i=n,j=m;
	while(f[i][j]!=0){
		if(s[i]==t[j]&&f[i][j]==f[i-1][j-1]+1){
			res[f[i][j]]=s[i];
			i--;j--; 
		}
		else if(f[i][j]==f[i-1][j])i--;
		else if(f[i][j]==f[i][j-1])j--;
	}
	for(int i=1;i<=f[n][m];i++){
		cout<<res[i];
	}
	return 0;
}

Longest Path

我们设 \(f_i\) 为以节点 \(i\) 为终点的最长路长度,于是对于每个 \(i\) 的前驱节点 \(t\),有转移方程:

\(f_i=\max(f_i,f_j+1)\)

写一个拓扑排序进行转移即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,h[N],e[N],ne[N],idx,din[N],f[N];
void add(int a,int b){
	e[idx]=b;ne[idx]=h[a];h[a]=idx++;din[b]++;
}
signed main(){
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=1;i<=m;i++){
		int a,b;
		cin>>a>>b;
		add(a,b);
	}
	queue<int>q;
	for(int i=1;i<=n;i++){
		if(din[i]==0){
			q.push(i);
		}
	}
	while(!q.empty()){
		int t=q.front();
		q.pop();
		for(int i=h[t];~i;i=ne[i]){
			int j=e[i];
			f[j]=max(f[j],f[t]+1);
			if(--din[j]==0)q.push(j);
		}
	}
	int res=0;
	for(int i=1;i<=n;i++){
		res=max(res,f[i]);
	}
	cout<<res;
	return 0;
}

Grid 1

我们设 \(f_{i,j}\) 表示走到 \((i,j)\) 格子的方案数。若 \(a_{1,1}\) 不为 #,则 \(f_{1,1}=1\)。于是在 \(a_{i,j}\) 不为 #,有转移方程:

\(f_{i,j}=f_{i-1,j}+f_{i,j-1}\)

这里给个思考题:

  • 没有障碍且 \(h,w\le 10^5\) 怎么做。

答案是 \(C_{h+w-2,h-1}\)。考虑为什么是这样?事实上一共要走 \(h+w-2\) 步,然后选择 \(i-1\) 步向下,其余的向右,所以是这样一个组合数问题。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 1005
#define mod 1000000007
using namespace std;
int n,m,f[N][N];
char a[N][N];
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>a[i][j];
		}
	}
	if(a[1][1]!='#')f[1][1]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(a[i][j]=='#')continue;
			if(i==1&&j==1)continue;
			f[i][j]=(f[i-1][j]+f[i][j-1])%mod;
		}
	}
	cout<<f[n][m];
	return 0;
}

Coins

我们设 \(f_{i,j}\) 为前 \(i\) 枚硬币,\(j\) 枚正面朝上的概率。初始化 \(f_{0,0}=1\)。转移方程有两种:

\(f_{i,j}=f_{i,j}+(1-p_i)\times f_{i-1,j}\)

\(j>0,f_{i,j}=f_{i,j}+p_i\times f_{i-1,j-1}\)

于是最终答案为 \(\displaystyle\sum_{i=\lfloor\frac{n}{2}\rfloor+1}^{i\le n}f_{n,i}\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 3005
using namespace std;
int n;
double p[N],f[N][N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>p[i];
	}
	f[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=i;j++){
			f[i][j]+=(1-p[i])*f[i-1][j];
			if(j!=0)f[i][j]+=p[i]*f[i-1][j-1];
		}
	}
	double res=0;
	for(int i=n/2+1;i<=n;i++){
		res+=f[n][i];
	}
	cout<<setiosflags(ios::fixed)<<setprecision(10);
	cout<<res;
	return 0;
}

Sushi

首先设 \(f_{i,j,k,l}\) 为当前剩余 \(i\) 个盘子中有 \(0\) 个寿司,\(j\) 个盘子中有 \(1\) 个寿司,\(k\) 个盘子中有 \(2\) 个寿司,\(l\) 个盘子中有 \(3\) 个寿司的期望吃完次数。

首先会发现空间炸了,但是我们先不管空间,先去推一下转移方程:

考虑分讨吃到的盘子中的寿司数量,于是有:

\(f_{i,j,k,l}=f_{i,j,k,l}\times p_1+f_{i+1,j-1,k,l}\times p_2+f_{i,j+1,k-1,l}\times p_3+f_{i,j,k+1,l-1}\times p_4+1\)。其中的变量 \(p_1=\frac{i}{n},p_2=\frac{j}{n},p_3=\frac{k}{n},p_4=\frac{l}{n}\)

移项得到:\(f_{i,j,k,l}=f_{i+1,j-1,k,l}\times\frac{j}{n-i}+f_{i,j+1,k-1,l}\times\frac{k}{n-i}+f_{i,j,k+1,l-1}\times\frac{l}{n-i}+\frac{n}{n-i}\)

然后我们有 \(i+j+k+l=n\),所以可以用 \(j+k+l\) 替换 \(n-i\)

发现现在的方程除了空间太大没有其他问题,又因为 \(i=j+k+l\),于是我们舍弃第一维 \(i\),把状态设为 \(f_{i,j,k}\)(注意这里 \(i\) 代表的是原来的 \(j\)\(j,k\) 以此类推),表示为当前剩余 \(i\) 个盘子中有 \(1\) 个寿司,\(j\) 个盘子中有 \(2\) 个寿司,\(k\) 个盘子中有 \(3\) 个寿司的期望吃完次数。

最终的方程:\(f_{i,j,k}=f_{i-1,j,k}\times\frac{i}{i+j+k}+f_{i+1,j-1,k}\times\frac{j}{i+j+k}+f_{i,j+1,k-1}\times\frac{k}{i+j+k}+\frac{n}{i+j+k}\),注意不要越界。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 305
using namespace std;
int n,a[N];
double f[N][N][N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		a[x]++;
	}
	for(int k=0;k<=n;k++){
		for(int j=0;j<=n;j++){
			for(int i=0;i<=n;i++){
				if(i==0&&j==0&&k==0)continue;
				double &v=f[i][j][k];
				if(i!=0)v+=f[i-1][j][k]*i/(i+j+k);
				if(j!=0)v+=f[i+1][j-1][k]*j/(i+j+k);
				if(k!=0)v+=f[i][j+1][k-1]*k/(i+j+k);
				v+=1.0*n/(i+j+k);
			}
		}
	}
	cout<<setiosflags(ios::fixed)<<setprecision(14);
	cout<<f[a[1]][a[2]][a[3]];
	return 0;
}

Stones

我们设 \(f_i\) 用来表示还剩 \(i\) 个石子时此人必胜还是必败(为 \(1\) 还是 \(0\))。

可以发现,\(f_i\) 能够转移到如果一个必败的状态,那么剩下 \(i\) 个石子时一定必胜,否则必败。

所以转移方程为:当存在一个 \(f_{i-a_j}=0\)\(f_i=1\),否则 \(f_i=0\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 105
#define K 100005
using namespace std;
int n,k,a[N],f[K];
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=k;i++){
		for(int j=1;j<=n;j++){
			if(i<a[j])continue;
			if(f[i-a[j]]==0)f[i]=1;
		}
	}
	if(f[k]==1)cout<<"First";
	else cout<<"Second";
	return 0;
}

Deque

我们设 \(f_{i,j}\) 为还剩下区间 \([i,j]\) 的数时,先手取到的数减去后手取到的数的最大值。可以发现这是一个区间 \(dp\)。于是我们分类讨论接下来该谁取数,这可以用已经取的数的奇偶性判断。

如果是该先手取数:\(f_{i,j}=\max(f_{i+1,j}+a_i,f_{i,j-1}+a_j)\)

如果是该后手取数:\(f_{i,j}=\min(f_{i+1,j}-a_i,f_{i,j-1}-a_j)\)

提示4:区间 \(dp\) 应该先枚举区间长度,再枚举左端点,然后根据这两个东西确定右端点。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 3005
using namespace std;
int n,a[N],f[N][N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int len=1;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			int c=n-len;
			if(len==1){
				if(c%2==0)f[i][j]=a[i];
				else f[i][j]=-a[i];
				continue;
			}
			if(c%2==0)f[i][j]=max(f[i+1][j]+a[i],f[i][j-1]+a[j]);
			else f[i][j]=min(f[i+1][j]-a[i],f[i][j-1]-a[j]);
		}
	}
	cout<<f[1][n];
	return 0;
}

Candies

这是我们的第一道 \(dp\) 优化,会进行比较详细的说明。

我们设 \(f_{i,j}\) 为前 \(i\) 个人分走 \(j\) 颗糖的方案数。初始化 \(f_{0,0}=1\),于是转移是好推的:

\(\displaystyle f_{i,j}=\sum_{k=\max(0,j-a_i)}^{j}f_{i-1,k}\)

然后就发现,这个东西的复杂度过高,所以要采取一些优化。

发现一个事情,我们慢的原因是每次都要求一次和,这个东西就是我们的瓶颈。

考虑对这个和式做前缀和,然后在转移的时候直接使用记录前缀和转移,这样就可以通过了。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 105
#define K 100005
#define mod 1000000007
using namespace std;
int n,k,a[N],f[N][K],sum[K];
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	f[0][0]=1;
	for(int i=1;i<=n;i++){
		sum[0]=f[i-1][0];
		for(int j=1;j<=k;j++){
			sum[j]=(sum[j-1]+f[i-1][j])%mod;
		}
		for(int j=0;j<=k;j++){
			int pos=max(0ll,j-a[i]);
			if(pos==0){
				(f[i][j]+=sum[j])%=mod;
			}
			else{
				(f[i][j]+=sum[j]-sum[pos-1])%=mod;
			}
			f[i][j]=(f[i][j]+mod)%mod;
		}
	}
	cout<<f[n][k];
	return 0;
}

Slimes

我们设 \(f_{i,j}\) 为合并区间 \([i,j]\) 的所有数的最小代价。于是我们对于所有的 \(k\in[i,j-1]\),有转移:

\(f_{i,j}=\min(f_{i,j},f_{i,k}+f_{k+1,j}+sum_j-sum_{i-1})\)

这里的 \(sum_i\) 为前缀和,代表 \(\displaystyle\sum_{j=1}^{i}a_j\)

这里为什么要加上 \([i,j]\) 区间所有数的和呢?是因为合并时的代价是两个数的和,而这两个数初始是这个区间的所有数,所以这两个数的和一定是区间 \([i,j]\) 内的数的和。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 405
using namespace std;
int n,a[N],sum[N],f[N][N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
	}
	memset(f,0x3f,sizeof f);
	for(int len=1;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			if(len==1){
				f[i][j]=0;
				continue;
			}
			for(int k=i;k<j;k++){
				f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
			}
		}
	}
	cout<<f[1][n];
	return 0;
}

Matching

我们设 \(f_i\) 为前 \(j\) 个男人与 \(i\) 集合中的女人的完备匹配方案数。其中 \(i\) 是一个状态压缩表示的集合,\(j\) 是集合 \(i\) 的大小。

所谓状态压缩,一般是用二进制表示某个东西的有无(当然也可能用其他进制表示一些复杂的东西)。这里 \(i\) 在二进制下的某一位如果是 \(1\),那么代表这个人在集合中。

于是有转移方程:\(f_i=f_i+f_{i-2^j}\),要求 \(j\)\(i\) 集合内,且 \(j\)\(k\) 之间有边(这里 \(k\) 是集合 \(i\) 的大小)。注意下面的代码因为下标从 \(0\) 开始,所以把 \(k\) 减去了 \(1\)

提示 \(5\):在表示一个集合且集合大小比较小时,可以用二进制状态压缩法表示。

提示 \(6\)__builtin_popcount(i) 是指 \(i\) 二进制表示下有多少位是 \(1\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 21
#define M 1<<N
#define mod 1000000007
#define pct __builtin_popcount
using namespace std;
int n,a[N][N],f[M];
signed main(){
	cin>>n;
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			cin>>a[i][j];
		}
	}
	f[0]=1;
	for(int i=1;i<1<<n;i++){
		int k=pct(i)-1;
		for(int j=0;j<n;j++){
			if((i>>j&1)&&(a[j][k]==1)){
				(f[i]+=f[i-(1<<j)])%=mod;
			}
		}
	}
	cout<<f[(1<<n)-1];
	return 0;
}

Independent Set

我们设 \(f_{i,0/1}\) 表示 \(i\) 节点染黑/白其子树的方案数。首先把所有的 \(f_{i,0/1}\) 初始化为 \(1\),然后对于所有 \(i\) 的儿子 \(j\),就有转移方程:

\(f_{i,0}=f_{i,0}\times f_{j,1}\)

\(f_{i,1}=f_{i,1}\times (f_{j,0}+f_{j,1})\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M N<<1
#define mod 1000000007
using namespace std;
int n,h[N],e[M],ne[M],idx,f[N][2];
void add(int a,int b){
	e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void dfs(int u,int fa){
	f[u][0]=f[u][1]=1;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==fa)continue;
		dfs(j,u);
		(f[u][0]*=f[j][1])%=mod;
		(f[u][1]*=(f[j][0]+f[j][1]))%=mod;
	}
}
signed main(){
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++){
		int a,b;
		cin>>a>>b;
		add(a,b);add(b,a);
	}
	dfs(1,0);
	cout<<(f[1][0]+f[1][1])%mod;
	return 0;
}

Flowers

首先引入一个东西,对于一个长度为 \(n\)\(n\le 5\times 10^3\) 的序列,求它的最长上升子序列的长度,只需设 \(f_i\) 为以 \(i\) 结尾的最长上升子序列的长度。

于是在 \(h_j<h_i\) 时,有转移方程:\(f_i=\max(f_i,f_j+1)\)

发现这样做有点慢,瓶颈在于遍历所有的 \(j\)。所以我们考虑能否快速查找。

事实上,我们可以把 \(f_i\) 插入进树状数组内以快速查询最大值。

现在回到这道题,我们设 \(f_i\) 表示以 \(i\) 结尾的最长上升子序列的花的权值和。

于是在 \(h_j<h_i\) 时,有转移方程:\(f_i=\max(f_i,f_j+a_j)\)

只需把 \(f_i\) 插入进树状数组内即可快速查询当 \(h_j<h_i\)\(f_j\) 的最大值。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,a[N],b[N],c[N],f[N];
int lowbit(int x){
	return x&-x;
}
void modify(int x,int v){
	while(x<=n){
		c[x]=max(c[x],v);
		x+=lowbit(x);
	}
}
int qry(int x){
	int res=0;
	while(x){
		res=max(res,c[x]);
		x-=lowbit(x);
	}
	return res;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		cin>>b[i];
		f[i]=qry(a[i]-1)+b[i];
		modify(a[i],f[i]);
	}
	int res=0;
	for(int i=1;i<=n;i++){
		res=max(res,f[i]);
	}
	cout<<res;
	return 0;
}

Walk

我们设 \(f_{t,i,j}\) 表示 \(i\rightarrow j\) 的长为 \(t\) 的路径。

于是有转移方程:\(\displaystyle f_{t,i,j}=\sum_{k=1}^n(f_{t-1,i,k}\times f_{1,k,j})\)

因为如果 \(i\rightarrow k\) 长为 \(t-1\)\(k\rightarrow j\) 长为 \(1\),故会产生一条 \(i\rightarrow j\) 长度为 \(t\) 的路径。

但是我们发现如果这样做 \(k\) 次,时间上不可接受。

观察一个东西,我们可以发现 \(f_t=f_{t-1}\times f_1\) 这等价于 \(f_t=f_1^t\)。于是我们使用矩阵快速幂进行优化。就是把 \(f_1\) 近似看成一个数,用快速幂和矩阵乘法的方法计算其 \(k\) 次方所对应的矩阵。

提示 \(7\):在矩阵乘法中代表单位 \(1\) 的矩阵具有 \(f_{i,i}=1\) 且其他值全为 \(0\) 的特点。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 55
#define mod 1000000007
using namespace std;
int n,k;
struct node{
	int f[N][N];
}a;
node operator*(node a,node b){
	node res;
	memset(res.f,0,sizeof res.f);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n;k++){
				(res.f[i][j]+=a.f[i][k]*b.f[k][j]%mod)%=mod;
			}
		}
	}
	return res;
}
node ksm(node x,int y){
	node res;
	memset(res.f,0,sizeof res.f);
	for(int i=1;i<=n;i++){
		res.f[i][i]=1;
	}
	while(y){
		if(y&1)res=res*x;
		x=x*x;
		y>>=1;
	}
	return res;
}
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			cin>>a.f[i][j];
		}
	}
	node b=ksm(a,k);
	int res=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			(res+=b.f[i][j])%=mod;
		}
	}
	cout<<res;
	return 0;
}

Digit Sum

我们考虑设 \(f_{i,j,1/0}\) 表示前 \(i\) 位的和对 \(d\) 取余的结果为 \(j\),并且当前的前 \(i\) 位是/否全部与上界相同。

于是我们可以使用记忆化搜索转移。转移方程:\(f_{i,j,k}=f_{i,j,k}+f_{i-1,(j+digit)\bmod d,k \operatorname{and} [digit=limit]}\)

这里 \(digit\) 为这一位的数,\(limit\) 为当前能选到的最大值。

转移边界为 \(i=0\) 时,如果 \(j\) 正好是 \(0\),则 \(f_{i,j,k}\)\(1\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 10005
#define D 105
#define mod 1000000007
using namespace std;
int d,num[N],cnt,f[N][D][2];
string k;
int dfs(int dep,int sum,bool is_lim){
	if(dep==0)return sum==0;
	int &v=f[dep][sum][is_lim];
	if(v!=-1)return v;
	int lim=9;
	if(is_lim)lim=num[dep];
	int res=0;
	for(int i=0;i<=lim;i++){
		(res+=dfs(dep-1,(sum+i)%d,is_lim&&(i==lim)))%=mod;
	}
	return v=res;
}
int solve(){
	for(int i=k.size()-1;i>=0;i--){
		num[++cnt]=k[i]-'0';
	}
	return dfs(cnt,0,1);
}
signed main(){
	memset(f,-1,sizeof f);
	cin>>k>>d;
	int res=solve()-1;
	res=(res+mod)%mod;
	cout<<res;
	return 0;
}

Permutation

我们设 \(f_{i,j}\) 表示前 \(i\) 位放 \(1\sim i\),且第 \(i\) 位在前 \(i\) 个数中是第 \(j\) 小的方案数。

于是我们分类讨论得出转移方程:

当字符为 <\(\displaystyle f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k}\)

当字符为 >\(\displaystyle f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k}\)

不难发现又是使用一个和式转移,所以我们对 \(f_{i-1}\) 记录前缀和,用前缀和优化转移即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 3005
#define mod 1000000007
using namespace std;
int n,f[N][N],sum[N];
char s[N];
signed main(){
	cin>>n;
	for(int i=2;i<=n;i++){
		cin>>s[i];
	}
	f[1][1]=1;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=i;j++){
			sum[j]=(sum[j-1]+f[i-1][j])%mod;
		}
		for(int j=1;j<=i;j++){
			if(s[i]=='<'){
				(f[i][j]+=sum[j-1])%=mod;
			}
			else{
				(f[i][j]+=sum[i-1]-sum[j-1])%=mod;
			}
			f[i][j]=(f[i][j]+mod)%mod;
		}
	}
	int res=0;
	for(int i=1;i<=n;i++){
		(res+=f[n][i])%=mod;
	}
	cout<<res;
	return 0;
}

Grouping

我们设 \(f_i\) 表示已选的集合为 \(i\) 的最大权值,于是对于每个 \(i\) 的子集 \(j\),有转移方程(注意 \(i,j\) 都是二进制数状态压缩表示的集合):

\(f_i=\max(f_i,f_j+val_{i-j})\)

这里 \(i-j\) 表示如果把 \(i-j\) 对应的集合全放到一组的贡献,事实上他可以预处理。

然后就是,枚举子集可以用位运算表示,复杂度 \(3^n\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 16
#define M 1<<N
using namespace std;
int n,f[M],val[M],a[N][N];
signed main(){
	cin>>n;
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			cin>>a[i][j];
		}
	}
	for(int i=0;i<1<<n;i++){
		for(int j=0;j<n;j++){
			if(!(i>>j&1))continue;
			for(int k=j+1;k<n;k++){
				if(!(i>>k&1))continue;
				val[i]+=a[j][k];
			}
		}
		f[i]=val[i];
	}
	for(int i=0;i<1<<n;i++){
		for(int j=i;j;j=j-1&i){
			f[i]=max(f[i],f[j]+val[i-j]);
		}
	}
	cout<<f[(1<<n)-1];
	return 0;
}

Subtree

我们设 \(f_u\) 为把 \(u\) 涂黑且连通块在 \(u\) 子树内的方案数,\(g_u\) 为把 \(u\) 涂黑且连通块不在 \(u\) 子树内的方案数。

于是对于 \(u\) 的儿子 \(j\) 有转移方程:\(\displaystyle f_u=\prod(f_j+1)\)

\(fa\)\(u\) 的父亲,那么对于 \(u\) 的兄弟 \(v\) 有转移方程:\(\displaystyle g_u=g_{fa}\times\prod(f_j+1)+1\)

\(g\) 这样转移的原因是:首先不在 \(u\) 子树内则要么在祖先处,要么在它兄弟子树内。而祖先处已经计算出了 \(g{fa}\),直接继承即可。后面的 \((f_j+1)\) 就是其兄弟子树内的方案,多出来的 \(1\) 是全染白色。

如果我们暴力找 \(u\) 的兄弟是会超时的,所以考虑怎么优化。

我们可以对于每个 \(u\),都预处理他前面的所有兄弟的 \((f_j+1)\) 之积和他后面的所有兄弟的 \((f_j+1)\) 之积。这样我们对于每个点,就可以用前后缀积的乘积相乘得到原先我们需要暴力计算的式子。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,mod,f[N],g[N],t1[N],t2[N];
vector<int>e[N];
void add(int a,int b){
	e[a].push_back(b);
}
void dfs1(int u,int fa){
	f[u]=1;
	vector<int>son;
	for(int j:e[u]){
		if(j==fa)continue;
		dfs1(j,u);
		(f[u]*=(f[j]+1))%=mod;
		son.push_back(j);
	}
	int p1=1,p2=1;
	for(int j:son){
		t1[j]=p1;
		(p1*=(f[j]+1))%=mod;
	}
	reverse(son.begin(),son.end());
	for(int j:son){
		t2[j]=p2;
		(p2*=(f[j]+1))%=mod;
	}
}
void dfs2(int u,int fa){
	if(fa==0)g[u]=1;
	else g[u]=(g[fa]*t1[u]%mod*t2[u]%mod+1)%mod;
	for(int j:e[u]){
		if(j==fa)continue;
		dfs2(j,u);
	}
}
signed main(){
	cin>>n>>mod;
	for(int i=1;i<n;i++){
		int a,b;
		cin>>a>>b;
		add(a,b);add(b,a);
	}
	dfs1(1,0);
	dfs2(1,0);
	for(int i=1;i<=n;i++){
		cout<<f[i]*g[i]%mod<<'\n';
	}
	return 0;
}

Intervals

我们先考虑一个朴素的 \(dp\),设 \(f_{i,j}\) 为考虑前 \(i\) 个位置,最后一个 1 放在 \(j\) 的最大分数。

于是有转移方程:\(\forall j<i,f_{i,j}=f_{i-1,j}+\sum_{l_k\le j,r_k=i}v_k\),其中 \(v_k\) 为第 \(k\) 个需求被满足获得的分数。注意这里 \(f_{i,i}\) 应该直接用所有 \(j\le i-1\)\(f_{i-1,j}\) 最大值更新 \(f_{i,i}\),再加上新产生的分数。

空间是好优化的,直接把 \(i\) 这一维去掉或者使用滚动数组优化都可以。

接下来考虑时间上的优化。可以发现每一次都是一个区间的 \(f\) 同时增加了一个值。不难发现这很像线段树的区间加操作。

于是,我们先把所有区间按照 \(r\) 排序,然后我们采用线段树维护 \(f\) 数组,每次区间加上一个分数,最后答案即为全局最大值。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,f[N<<2],lzy[N<<2];
struct node{
	int l,r,v;
	bool operator<(const node &t)const{
		return r<t.r;
	}
}a[N];
void pushup(int u){
	f[u]=max(f[u<<1],f[u<<1|1]);
}
void pushdown(int u){
	f[u<<1]+=lzy[u];
	f[u<<1|1]+=lzy[u];
	lzy[u<<1]+=lzy[u];
	lzy[u<<1|1]+=lzy[u];
	lzy[u]=0;
}
void modify(int u,int l,int r,int L,int R,int v){
	if(l>=L&&r<=R){
		f[u]+=v;
		lzy[u]+=v;
		return;
	}
	int mid=l+r>>1;
	pushdown(u);
	if(L<=mid)modify(u<<1,l,mid,L,R,v);
	if(R>mid)modify(u<<1|1,mid+1,r,L,R,v);
	pushup(u);
}
int qry(){
	return max(f[1],0ll);
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a[i].l>>a[i].r>>a[i].v;
	}
	sort(a+1,a+m+1);
	int j=1;
	for(int i=1;i<=n;i++){
		modify(1,1,n,i,i,qry());
		while(j<=m&&a[j].r==i){
			modify(1,1,n,a[j].l,a[j].r,a[j].v);
			j++;
		}
	}
	cout<<qry();
	return 0;
}

Tower

考虑 \(i,j\) 谁放到上面会更优。如果 \(i\)\(j\) 上面,则剩下 \(s_j-w_i\),否则剩下 \(s_i-w_j\)。考虑移项,得出 \(s_i+w_i>s_j+w_j\)\(i\) 在下面更优,否则 \(j\) 在下面更优。

于是我们按照这个排个序,然后写一个背包就做完了。

具体地,设 \(f_j\) 表示重量为 \(j\) 时的最大价值,有 \(f_j=\max(f_j,f_{j-w_i}+v_i)\),注意不要越界。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 1005
#define M 20005
using namespace std;
int n,f[M];
struct node{
	int w,s,v;
	bool operator<(const node &t)const{
		return w+s<t.w+t.s;
	}
}a[N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].w>>a[i].s>>a[i].v;
	}
	sort(a+1,a+n+1);
	int res=0;
	for(int i=1;i<=n;i++){
		for(int j=a[i].w+a[i].s;j>=a[i].w;j--){
			f[j]=max(f[j],f[j-a[i].w]+a[i].v);
			res=max(res,f[j]);
		}
	}
	cout<<res;
	return 0;
}

Grid 2

首先之前提到过一个结论,对于没有障碍的网格,从 \((1,1)\) 走到 \((n,m)\) 的方案数为 \(C_{n+m-2}^{n-1}\),这个不难理解。

于是我们设 \(f_i\) 为走到第 \(i\) 个障碍的合法方案数,于是 \(f_i\) 的初始化就为上面的那个式子。

接下来,我们找到所有走到第 \(i\) 个障碍所必须经过的障碍 \(j\),事实上这里就是对坐标排个序然后找出 \(x_j\le x_i\)\(y_j\le y_i\) 的所有 \(j\)。然后就有转移方程:

\(f_i=f_i-f_j\times C_{x_i-x_j+y_i-y_j}^{x_i-x_j}\)

最后就是把终点当作第 \(n+1\) 个障碍,答案即为 \(f_{n+1}\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 3005
#define M 200005
#define mod 1000000007
using namespace std;
int h,w,n,fac[M],inv[M],f[N];
struct node{
	int x,y;
	bool operator<(const node &t)const{
		if(x==t.x)return y<t.y;
		return x<t.x;
	}
}a[N];
int ksm(int x,int y){
	int res=1;
	while(y){
		if(y&1)(res*=x)%=mod;
		(x*=x)%=mod;
		y>>=1;
	}
	return res;
}
void init(){
	fac[0]=1;
	inv[0]=1;
	for(int i=1;i<M;i++){
		fac[i]=fac[i-1]*i%mod;
		inv[i]=ksm(fac[i],mod-2);
	}
}
int c(int n,int m){
	return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main(){
	init();
	cin>>h>>w>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].y;
	}
	a[++n]={h,w};
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++){
		f[i]=c(a[i].x+a[i].y-2,a[i].x-1);
		for(int j=1;j<i;j++){
			if(a[j].y>a[i].y)continue;
			f[i]-=f[j]*c(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod;
			f[i]=(f[i]+mod)%mod;
		}
	}
	cout<<f[n];
	return 0;
}

Frog 3

我们设 \(f_i\) 表示跳到第 \(i\) 个石头的最小花费。可以发现朴素转移是会超时的,于是考虑优化。

首先给出方程:\(f_i=\min(f_i,f_j+(h_i-h_j)^2+c)\),考虑能否快速求出后面这个东西的最小值。

我们把它列出来:\(f_j+(h_i-h_j)^2+c\),然后展开:\(f_j+h_i^2+h_j^2-2\times h_i\times h_j+c\)。可以发现只含 \(i\) 的项和 \(c\) 可以看作常量。

我们假设 \(j_1\)\(j_2\) 转移更优,于是有:\(f_{j_1}+h_{j_1}^2-2\times h_i\times h_{j_1}\le f_{j_2}+h_{j_2}^2-2\times h_i\times h_{j_2}\)。我们再设 \(g_j\)\(f_j+h_j^2\),以及 \(k\)\(-2\times h_i\)。可推出:\(g_{j_1}-2\times k\times h_{j_1}\le g_{j_2}-2\times k\times h_{j_2}\)

注意 \(h\) 是单调递增的,然后把上面的式子做一个移项得到:\(-2\times k\times(h_{j_1}-h_{j_2})\le g_{j_2}-g_{j_1}\),除过去,得到:\(-2\times k\ge\frac{g_{j_2}-g_{j_1}}{h_{j_1}-h_{j_2}}\),两边同时变号得:\(2\times k\le\frac{g_{j_1}-g_{j_2}}{h_{j_1}-h_{j_2}}\)

这里我们可以把 \(j\) 看成横坐标,\(g_j\) 看成纵坐标,这样右边的东西就可以看成是斜率。然后维护一个下凸壳。具体地,因为 \(2\times h_i\) 单调递增,所以采用一个单调队列维护斜率,每次当队头的斜率小于 \(2\times h_i\) 就弹出队头,然后使用队头对 \(i\) 转移,最后维护一下队尾的单调性,然后单调队列内加入 \(i\)

代码:

#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,c,h[N],q[N],f[N];
double get(int i,int j){
	return ((f[i]+h[i]*h[i])-(f[j]+h[j]*h[j]))*1.0/(h[i]-h[j]);
}
signed main(){
	cin>>n>>c;
	for(int i=1;i<=n;i++){
		cin>>h[i];
	}
	int hh=0,tt=0;
	q[tt]=1;
	for(int i=2;i<=n;i++){
		while(hh<tt&&get(q[hh],q[hh+1])<2*h[i])hh++;
		f[i]=f[q[hh]]+(h[i]-h[q[hh]])*(h[i]-h[q[hh]])+c;
		while(hh<tt&&get(q[tt-1],q[tt])>get(q[tt],i))tt--;
		q[++tt]=i;
	}
	cout<<f[n];
	return 0;
}
posted @ 2024-08-18 18:38  zxh923  阅读(10)  评论(0编辑  收藏  举报