dp专题训练

Frog 1

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

fi=min(fi1+|hi1hi|,fi2+|hi2hi|)

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

于是做一个线性 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

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

fi=minj=max(1,ik)j<i(fj+|hihj|)

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

这里给个思考题:

  • k=n 怎么做?

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

代码:

#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

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

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

转移方程非常好推:fi,j=maxjji(fi1,j+valj)valj 为第 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

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

fi,j=maxjjwi(fi1,jwi+vi)

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

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

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

事实上可以继续优化。我们可以把第一维直接优化掉,我们在循环 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

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

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

  • fi,j=min(fi,j,fi1,j)

  • fi,j=minjjvi(fi,j,fi1,jvi+wi)

答案非常好求,就是找到最大的 i,使得 fn,im,答案就是这个 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

最长公共子序列的状态十分经典。设 fi,js 的前 i 个字符和 t 的前 j 个字符的最长公共子序列的长度。转移方程:

fi,j=max(fi1,j,fi,j1)

sitj 相同时,fi,j=max(fi,j,fi1,j1+1)

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

分三种情况讨论:

  • sitj 相同且 fi,j=fi1,j1+1:那么答案的第 fi,j 位就是 si。然后 i=i1,j=j1

  • fi,jfi1,j 相同:i=i1

  • fi,jfi,j1 相同:j=j1

这里事实上就是枚举 fi,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

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

fi=max(fi,fj+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

我们设 fi,j 表示走到 (i,j) 格子的方案数。若 a1,1 不为 #,则 f1,1=1。于是在 ai,j 不为 #,有转移方程:

fi,j=fi1,j+fi,j1

这里给个思考题:

  • 没有障碍且 h,w105 怎么做。

答案是 Ch+w2,h1。考虑为什么是这样?事实上一共要走 h+w2 步,然后选择 i1 步向下,其余的向右,所以是这样一个组合数问题。

代码:

#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

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

fi,j=fi,j+(1pi)×fi1,j

j>0,fi,j=fi,j+pi×fi1,j1

于是最终答案为 i=n2+1infn,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

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

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

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

fi,j,k,l=fi,j,k,l×p1+fi+1,j1,k,l×p2+fi,j+1,k1,l×p3+fi,j,k+1,l1×p4+1。其中的变量 p1=in,p2=jn,p3=kn,p4=ln

移项得到:fi,j,k,l=fi+1,j1,k,l×jni+fi,j+1,k1,l×kni+fi,j,k+1,l1×lni+nni

然后我们有 i+j+k+l=n,所以可以用 j+k+l 替换 ni

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

最终的方程:fi,j,k=fi1,j,k×ii+j+k+fi+1,j1,k×ji+j+k+fi,j+1,k1×ki+j+k+ni+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

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

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

所以转移方程为:当存在一个 fiaj=0fi=1,否则 fi=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

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

如果是该先手取数:fi,j=max(fi+1,j+ai,fi,j1+aj)

如果是该后手取数:fi,j=min(fi+1,jai,fi,j1aj)

提示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 优化,会进行比较详细的说明。

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

fi,j=k=max(0,jai)jfi1,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

我们设 fi,j 为合并区间 [i,j] 的所有数的最小代价。于是我们对于所有的 k[i,j1],有转移:

fi,j=min(fi,j,fi,k+fk+1,j+sumjsumi1)

这里的 sumi 为前缀和,代表 j=1iaj

这里为什么要加上 [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

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

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

于是有转移方程:fi=fi+fi2j,要求 ji 集合内,且 jk 之间有边(这里 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

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

fi,0=fi,0×fj,1

fi,1=fi,1×(fj,0+fj,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

首先引入一个东西,对于一个长度为 nn5×103 的序列,求它的最长上升子序列的长度,只需设 fi 为以 i 结尾的最长上升子序列的长度。

于是在 hj<hi 时,有转移方程:fi=max(fi,fj+1)

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

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

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

于是在 hj<hi 时,有转移方程:fi=max(fi,fj+aj)

只需把 fi 插入进树状数组内即可快速查询当 hj<hifj 的最大值。

代码:

#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

我们设 ft,i,j 表示 ij 的长为 t 的路径。

于是有转移方程:ft,i,j=k=1n(ft1,i,k×f1,k,j)

因为如果 ik 长为 t1kj 长为 1,故会产生一条 ij 长度为 t 的路径。

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

观察一个东西,我们可以发现 ft=ft1×f1 这等价于 ft=f1t。于是我们使用矩阵快速幂进行优化。就是把 f1 近似看成一个数,用快速幂和矩阵乘法的方法计算其 k 次方所对应的矩阵。

提示 7:在矩阵乘法中代表单位 1 的矩阵具有 fi,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

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

于是我们可以使用记忆化搜索转移。转移方程:fi,j,k=fi,j,k+fi1,(j+digit)modd,kand[digit=limit]

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

转移边界为 i=0 时,如果 j 正好是 0,则 fi,j,k1

代码:

#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

我们设 fi,j 表示前 i 位放 1i,且第 i 位在前 i 个数中是第 j 小的方案数。

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

当字符为 <fi,j=k=1j1fi1,k

当字符为 >fi,j=k=ji1fi1,k

不难发现又是使用一个和式转移,所以我们对 fi1 记录前缀和,用前缀和优化转移即可。

代码:

#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

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

fi=max(fi,fj+valij)

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

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

代码:

#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

我们设 fu 为把 u 涂黑且连通块在 u 子树内的方案数,gu 为把 u 涂黑且连通块不在 u 子树内的方案数。

于是对于 u 的儿子 j 有转移方程:fu=(fj+1)

fau 的父亲,那么对于 u 的兄弟 v 有转移方程:gu=gfa×(fj+1)+1

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

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

我们可以对于每个 u,都预处理他前面的所有兄弟的 (fj+1) 之积和他后面的所有兄弟的 (fj+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,设 fi,j 为考虑前 i 个位置,最后一个 1 放在 j 的最大分数。

于是有转移方程:j<i,fi,j=fi1,j+lkj,rk=ivk,其中 vk 为第 k 个需求被满足获得的分数。注意这里 fi,i 应该直接用所有 ji1fi1,j 最大值更新 fi,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 谁放到上面会更优。如果 ij 上面,则剩下 sjwi,否则剩下 siwj。考虑移项,得出 si+wi>sj+wji 在下面更优,否则 j 在下面更优。

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

具体地,设 fj 表示重量为 j 时的最大价值,有 fj=max(fj,fjwi+vi),注意不要越界。

代码:

#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) 的方案数为 Cn+m2n1,这个不难理解。

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

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

fi=fifj×Cxixj+yiyjxixj

最后就是把终点当作第 n+1 个障碍,答案即为 fn+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

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

首先给出方程:fi=min(fi,fj+(hihj)2+c),考虑能否快速求出后面这个东西的最小值。

我们把它列出来:fj+(hihj)2+c,然后展开:fj+hi2+hj22×hi×hj+c。可以发现只含 i 的项和 c 可以看作常量。

我们假设 j1j2 转移更优,于是有:fj1+hj122×hi×hj1fj2+hj222×hi×hj2。我们再设 gjfj+hj2,以及 k2×hi。可推出:gj12×k×hj1gj22×k×hj2

注意 h 是单调递增的,然后把上面的式子做一个移项得到:2×k×(hj1hj2)gj2gj1,除过去,得到:2×kgj2gj1hj1hj2,两边同时变号得:2×kgj1gj2hj1hj2

这里我们可以把 j 看成横坐标,gj 看成纵坐标,这样右边的东西就可以看成是斜率。然后维护一个下凸壳。具体地,因为 2×hi 单调递增,所以采用一个单调队列维护斜率,每次当队头的斜率小于 2×hi 就弹出队头,然后使用队头对 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 @   zxh923  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示