从零开始的dp做题记录

懒得修 LaTeX

P2340 [USACO03FALL] Cow Exhibition G

变化了一些的01背包,还有要注意情商和智商为负的情况,分类讨论转移。

至于背包的容量珂以看作上界为 8e5,下界为 0

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,maxn=400000;
ll n,s[M],f[M],vis[M],ans;
ll dp[M];
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>s[i]>>f[i];
	memset(dp,-63,sizeof(dp));
	dp[maxn]=0;
	for(int i=1;i<=n;++i){
		if(s[i]>=0)
			for(int j=2*maxn;j>=s[i];--j)
				dp[j]=max(dp[j],dp[j-s[i]]+f[i]);
		else
			for(int j=0;j<=2*maxn+s[i];++j)
				dp[j]=max(dp[j],dp[j-s[i]]+f[i]);
	}
	for(int i=maxn;i<=2*maxn;++i)
		if(dp[i]>0) ans=max(ans,dp[i]+i-maxn);
	cout<<ans;
	return 0;
}

P4141 消失之物

赵队是消失之物

也是01背包变种,可以先跑一遍普通的01背包,再每次把多转移的部分减掉。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810;
ll n,m,w[N],dp[N],dp_[N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>w[i];
	dp[0]=1;
	for(int i=1;i<=n;++i)
		for(int j=m;j>=w[i];--j)
			dp[j]+=dp[j-w[i]],dp[j]%=10;
	for(int i=1;i<=n;++i){
		dp_[0]=1;
		for(int j=1;j<=m;++j){
			if(w[i]>j) dp_[j]=dp[j]%10;
			else dp_[j]=(dp[j]-dp_[j-w[i]]+10)%10; //避免负数 
			cout<<dp_[j];
		}
		cout<<'\n';
	} 
	return 0;
}

P1273 有线电视网

据说是珂以看作分组背包,把每个点都看成一个分组背包来求解。

dpi,j 为以 i 为根的子树中选 j 个人时的最大收益。这种不直接设答案所求值的状态也是一种技巧。

转移:dpu,j=max(dpu,j,dpu,jk+dpv,kw)

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3005,M=1919810;
ll n,m,pay[N];
struct xx{
	ll next,to,val;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
}
ll dp[N][N]; //设dp[i][j]为以i为根的子树中满足j个人时的利润
/*统计答案时取dp[1][i]大于0时最大的i
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-w)*/
ll dfs(ll u,ll fa){
	if(pay[u]){ //叶节点 
		dp[u][1]=pay[u];
		return 1;
	}
	ll sum=0;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		ll val=dfs(v,u); sum+=val;
		for(int j=sum;j>0;--j)
			for(int k=1;k<=val;++k)
				if(j-k>=0) dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-w);
	}
	return sum;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n-m;++i){
		ll k,a,c;
		cin>>k;
		for(int j=1;j<=k;++j){
			cin>>a>>c;
			add(i,a,c),add(a,i,c);
		}
	}
	for(int i=1;i<=m;++i) cin>>pay[i+n-m];
	memset(dp,-63,sizeof(dp));
	for(int i=1;i<=n;++i) dp[i][0]=0;
	dfs(1,0);
	ll ans=0;
	for(int i=1;i<=m;++i)
		if(dp[1][i]>=0) ans=max(ans,i*1ll);
	cout<<ans;
	return 0;
}

CF1882D

赛时看出来是一个换根,不会写/ll

dpi 为把 i 为根的子树变成一样的最小代价。先预处理出子树大小和每个节点的异或代价,然后一遍dfs转移。

可以证明到对于一个子树把他转化成跟根一样的值的代价是最小的,那么这样就方便我们直接转移了。

dpi=Σjsoni(dpj+sizej×(aiaj))

转移:dpv=(nsizev)×(auav)+dpusizev×(auav)

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*1145140;
ll t;
ll n,a[N];
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll size[N],val[N];
void dfs_pre(ll u,ll fa){
	size[u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_pre(v,u);
		size[u]+=size[v];
		val[u]+=val[v]+size[v]*(a[u]^a[v]);
	}
}
ll dp[N];
void dfs_dp(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dp[v]=(n-size[v])*(a[u]^a[v])+dp[u]-size[v]*(a[u]^a[v]); 
		dfs_dp(v,u);
	}
}
void solve(){
	cin>>n;
	cnt=0;
	for(int i=1;i<=n;++i) cin>>a[i],dp[i]=val[i]=size[i]=0;
	for(int i=1;i<n;++i){
		ll u,v;
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	if(n==1){
		cout<<"0\n";
		return;
	}
	dfs_pre(1,0);
	dp[1]=val[1]; 
	dfs_dp(1,0);
	for(int i=1;i<=n;++i) cout<<dp[i]<<" ";
	cout<<'\n';
	for(int i=1;i<=2*n;++i) head[i]=0;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>t;
	while(t--) solve();
	return 0;
}

P1122最大子树和

太简单了直接一遍dfs每次取max就行了

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,a[N],dp[N]; //dp[u]为dp到第i个点时的最大值 
void dfs(ll u,ll fa){
	dp[u]=a[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u]=max(dp[u],dp[u]+dp[v]);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<n;++i){
		ll u,v;
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	dfs(1,0);
	ll ans=-inf;
	for(int i=1;i<=n;++i) ans=max(ans,dp[i]);
	cout<<ans;
	return 0;
}

P1944最长括号匹配

我去一开始还没想出来/lh

很简单,设 dpi 为以 si 为结尾的最长括号串的长度。

考虑刷表法。很明显能够转移仅限于 si 为)或],那么能和他匹配的位置就是 i1dpi1,如果他们能匹配那么 dpi=2+dpi1+dpi2dpi1,注意还要加上上一个合法括号串的长度。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810;
ll n,dp[M]; //dp[i]表示以s[i]结尾的最大长度 
string s;
int main(){
	cin>>s;
	n=s.size();
	for(int i=1;i<s.size();++i)
		if((s[i]==')'&&s[i-1-dp[i-1]]=='(')||(s[i]==']'&&s[i-1-dp[i-1]]=='['))
			dp[i]=dp[i-2-dp[i-1]]+2+dp[i-1];
	ll ans=0;
	for(int i=1;i<=n;++i) ans=max(ans,dp[i]);
	for(int i=1;i<=n;++i)
		if(ans==dp[i]){
			for(int j=i-ans+1;j<=i;++j) cout<<s[j];
			return 0;
		}
	return 0;
}

P4310绝世好题

确实绝世好题,非常的妙。

dpi 为目前的数第 i 位为 1 时的最大序列长度。这样子在读入的时候就可以直接枚举 i 转移,时间复杂度 O(nlogV),因为有个 30

当然像最长上升子序列那样 O(n2) 搞有90pts。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810;
ll n,dp[32],a[M];
//设dp[i]为目前最后一个数第i位为1时的最长长度?
//确实绝世好题 
int main(){
	cin>>n;
	ll ans=0,len=0;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		len=1;
		for(int j=0;j<=30;++j)
			if((1<<j)&a[i]) len=max(dp[j]+1,len);
		for(int j=0;j<=30;++j)
			if((1<<j)&a[i]) dp[j]=max(dp[j],len);
		ans=max(ans,len);
	}
	cout<<ans;
	return 0;
}

P4084 [USACO17DEC] Barn Painting G

较简单的树形dp。设 dpu,iu 点颜色为 i 时的合法方案数,从叶往根转移,最后答案是根的三种颜色方案数之和。

记得取模就好。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810,mod=1e9+7;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,k,dp[N][4],c[N];
//设dp[u][j]为u点颜色为j时的方案数 
void dfs(ll u,ll fa){
	if(c[u]) dp[u][c[u]]=1;
	else dp[u][1]=dp[u][2]=dp[u][3]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][1]=dp[u][1]*(dp[v][2]+dp[v][3])%mod;
		dp[u][2]=dp[u][2]*(dp[v][1]+dp[v][3])%mod;
		dp[u][3]=dp[u][3]*(dp[v][1]+dp[v][2])%mod;
	}
}
int main(){
	cin>>n>>k;
	for(int i=1;i<n;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	for(int i=1;i<=k;++i){
		ll a,b;
		cin>>a>>b;
		c[a]=b;
	}
	dfs(1,0);
	cout<<(dp[1][1]+dp[1][2]+dp[1][3])%mod;
	return 0;
}

P3047 [USACO12FEB] Nearby Cows G

发现是换根dp就好做了。珂以设 dpu,iu 子树内距离 u 点距离 i 的点的点权和。

第一遍dfs把这个dp数组处理出来,第二遍就是换根dp求解。还有就是要注意距离不超过 k 的点也包括自身。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810,mod=1e9+7;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,k,c[N],dp[N][21]; //设dp[u][i]为u子树内离u点距离为i的点的点权和 
//这样设可以整一个换根dp
void dfs_pre(ll u,ll fa){
	dp[u][0]=c[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_pre(v,u);
		for(int j=1;j<=k;++j) dp[u][j]+=dp[v][j-1];
	}
}
ll ans[N];
void dfs_dp(ll u,ll fa){
	for(int i=0;i<=k;++i) ans[u]+=dp[u][i]; //不是为什么要加自己啊? 
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		for(int j=1;j<=k;++j) dp[u][j]-=dp[v][j-1];
		for(int j=1;j<=k;++j) dp[v][j]+=dp[u][j-1];
		dfs_dp(v,u);
		for(int j=1;j<=k;++j) dp[v][j]-=dp[u][j-1];
		for(int j=1;j<=k;++j) dp[u][j]+=dp[v][j-1];
	}
}
int main(){
	cin>>n>>k;
	for(int i=1;i<n;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	for(int i=1;i<=n;++i) cin>>c[i];
	dfs_pre(1,0);
	dfs_dp(1,0);
	for(int i=1;i<=n;++i) cout<<ans[i]<<'\n';
	return 0;
}

P1282 多米诺骨牌

可以看作背包,但是我没写。设 dp[i] 为前 i 张牌的第一行和为 j 时的最小反转次数,然后每次 j 枚举到 6n

这样设计状态珂以避免状态表示中出现负数,通过表示一个量同时知道总和来求出两个量之间的差。

统计答案的时候不断找最小差值然后顺便记录下最小反转次数。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1005,M=1919810;
ll a[N],b[N],inf;
ll n,sum[N],dp[N][6*N]; //设dp[i][j]为前i张牌的第一行和为j时的最小反转次数 
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i]>>b[i],sum[i]=sum[i-1]+a[i]+b[i];
	memset(dp,63,sizeof(dp)); inf=dp[0][0];
	dp[1][a[1]]=0; dp[1][b[1]]=(a[1]!=b[1]); //有可能点数相等翻不翻都一样 
	for(int i=2;i<=n;++i)
		for(int j=0;j<=6*n;++j){
			if(j-a[i]>=0) dp[i][j]=min(dp[i][j],dp[i-1][j-a[i]]); //不反转 
			if(j-b[i]>=0) dp[i][j]=min(dp[i][j],dp[i-1][j-b[i]]+1); //反转 
		}
	ll ans=inf,res=inf;
	for(int i=0;i<=sum[n];++i)
		if(dp[n][i]!=inf){
			if(abs(i-(sum[n]-i))<res)
				res=abs(i-(sum[n]-i)),ans=dp[n][i];
			else if(abs(i-(sum[n]-i))==res) ans=min(ans,dp[n][i]); 
		}
	cout<<ans;
	return 0;
}

P4158 [SCOI2009] 粉刷匠

有一定难度了,状态的设计能带来一些启示。

可以先对每一行预处理一个前缀和辅助转移,然后珂以设 dp[i][j] 为前 i 行刷 j 次最大能正确粉刷的格子数。

但是会发现这样根本转移不了!这个时候我们珂以转换思路,设计另一个辅助数组 dpp,表示第 i 行刷了前 j 个格子并且刷了 k 次时的最大正确格子数。

这样子我们就珂以先求出 dpp 数组,然后再利用 dpp 辅助转移 dp 数组求出答案 dp[n][t]

那dpp怎么转移呢?首先珂以肯定 dpp[i][j][k] 是由 dpp[i][j][k1] 转移而来的,那增加的值就是新的正确的格子,那么预处理的前缀和就能用上了。

珂以再枚举一个变量 l 表示 dpp[i][j][k1] 刷对了前 l 个格子,那么新增的蓝/红格子便是 num[i][j]num[i][l],jl(num[i][j]num[i][l])

转移便是:dpp[i][j][k]=max(dpp[i][j][k],dpp[i][l][k1]+max(num[i][j]num[i][l],jl(num[i][j]num[i][l])))

那么这下 dp 数组也方便转移了,转移:dp[i][j]=max(dp[i][j],dp[i1][jk]+dpp[i][m][k])(1jt,0kmin(j,m))

总结:有时候直接设一个 dp 数组推不出来的时候珂以再设一个辅助数组,先把他推出来再进行求解。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=55,M=1919810;
ll n,m,t,inf;
ll f[N][N]; char ch;
ll num[N][N]; //得整成前缀和,要不然转移过程用不了
ll dpp[N][N][2505]; //表示第i行刷了前j个格子并且刷了k次时的最大正确数
//这个时候k只用枚举到m
ll dp[N][2505]; //表示前i行刷j次最大能正确粉刷的格子数
int main(){
	cin>>n>>m>>t;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j){
			cin>>ch,f[i][j]=ch-'0';
			num[i][j]=num[i][j-1]+f[i][j];
		}
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			for(int k=1;k<=m;++k)
				for(int l=0;l<j;++l) //O(n^4)还好 
	dpp[i][j][k]=max(dpp[i][j][k],dpp[i][l][k-1]+max(num[i][j]-num[i][l],j-l-(num[i][j]-num[i][l])));
	for(int i=1;i<=n;++i)
		for(int j=1;j<=t;++j)
			for(int k=0;k<=j;++k) //直接小于等于j也可以,就是数组要开大点
				dp[i][j]=max(dp[i][j],dp[i-1][j-k]+dpp[i][m][k]);
	cout<<dp[n][t];
	return 0;
}

P1063能量项链

最基础的区间dp。区间dp一般思路:状态用来表示一个区间的答案,然后在区间中枚举一个位置分割区间来由小到大转移。

还有一个很基础的操作:破环成链,就是直接把数组乘二而已。

这个题也就相当于板子了,虽然dp不存在什么板子。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114,M=1919810;
ll n,a[N],dp[N][N]; //鸽了快两年了
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],a[i+n]=a[i];
	for(int len=2;len<=n+1;++len)
		for(int i=1;i<=2*n-len+1;++i){
			ll j=i+len-1;
			for(int k=i+1;k<j;++k)
				dp[i][j]=max(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[k]*a[j]);
		}
	ll ans=0;
	for(int i=1;i<=n;++i) ans=max(ans,dp[i][n+i]);
	cout<<ans;
	return 0;
}

P4170涂色

也是个基础的区间dp,设 dp[i][j] 为区间 [i,j] 达成目标状态的最小操作次数。

每个长度为一的区间操作次数都是 1,如果区间的两端相同那么可以由向内一格的区间转移而来。如果不相同就枚举 k 转移。

就是不知道为什么那个赋值为 1 的初始化没作用,要多整几个地方才能生效。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114,M=1919810;
ll n; char s[N];
ll dp[N][N]; //表示区间[i,j]达成目标状态的最小操作次数 
int main(){
	scanf("%s",s+1); n=strlen(s+1);
	memset(dp,63,sizeof(dp));
	for(int i=1;i<=n;++i) dp[i][i]=1;
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			if(i==j) dp[i][j]=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]);
			}
			if(i==j) dp[i][j]=1;
		}
	cout<<dp[1][n];
	return 0;
} 

P3147 248 G

基础。设为区间 [i,j] 能达成的最大数,初始 dp[i][i]=a[i]

然后枚举 k 判断,如果 dp[i][k]=dp[k+1][j],那么可以转移,并且 dp[i][k] 要有值。总复杂度 O(n3)

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=300,M=1919810;
ll n,a[N];
ll dp[N][N]; //设为[i,j]能合并出来的最大值 
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],dp[i][i]=a[i];
	for(int len=2;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			for(int k=i;k<j;++k)
				if(dp[i][k]==dp[k+1][j]&&dp[i][k]) //我谔谔 
					dp[i][j]=max(dp[i][j],dp[i][k]+1);
			//cout<<i<<" "<<j<<" "<<dp[i][j]<<'\n';
		}
	ll ans=0;
	for(int i=1;i<=n;++i)
		for(int j=i;j<=n;++j)
			ans=max(ans,dp[i][j]);
	cout<<ans;
	return 0;
} 

P3147 262144 P

上面那道题的加强版,要求大概在 O(nlogn) 内。

这个时候就要重新设状态了,珂以不直接设最大值,而是设 dp[i][j] 为第 i 个数要变成 j 所用的最大操作次数,相当于是以 i 为左端点, dp[i][j] 为右端点的一段区间。

首先珂以知道他能由 dp[i][j1] 转移来,那么我们以此类推,dp[i][j1] 就是由 dp[dp[i][j1]][j1] 得来的。这里的构造方式很像或者说就是倍增。

那么转移就这样了,dp[i][j] 有值的时候就取 max(j) 就行了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=300010,M=1919810;
ll n,a[N],ans;
ll dp[N][61]; //需要O(nlogn)做法
//设为第i个数合并为j所需最小次数,相当于一段区间的右端点 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],dp[i][a[i]]=i+1;
	for(int j=1;j<=60;++j)
	for(int i=1;i<=n;++i){
		if(dp[i][j]==0)
			dp[i][j]=dp[dp[i][j-1]][j-1]; //有点倍增的感觉
		if(dp[i][j]) ans=max(ans,j*1ll);
	}
	cout<<ans;
	return 0;
} 

CF149D Coloring Brackets

一个不太一样的区间dp。首先把匹配的括号对处理出来是肯定的,然后直接设 dp[i][j] 表示区间 [i,j] 的方案数会发现也是转移不了,似乎也没有什么可以拿来辅助转移的。

我们让状态贴近题面,设 dp[i][j][0/1/2][0,1,2] 表示 i 颜色为 0/1/2j 颜色 0/1/2 时区间 [i,j] 的方案数。但是直接循环转移还是不好转移因为括号串的匹配不是按顺序来的的,好像还是过不了,有没有能保证正确性还能在 O(n3) 内转移的方式呢?

有,记忆化搜索。直接从 [1,n] 开始递归就可以了。

然后是转移,基本就是分类讨论。如果 l+1=r,那么直接将 dp[i][j][0][1]/[0][2]/[1][0]/[2][0] 赋成1就好。如果是一对匹配的括号那就从 dp[l+1][r1] 转移过来,转移的时候还要注意判断相邻的颜色需要不同。

否则的话就从 dp[l][pair[l]]dp[pair[l]+1][r] 相乘转移过来,直接四重循环枚举每个颜色,还是要注意相邻颜色不相同。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=705,M=1919810,mod=1e9+7;
char s[N];
ll p[N]; stack <ll> st;
ll n,dp[N][N][3][3];
/*设dp[i][j][0/1/2][0/1/2]为区间[i,j]内i点颜色0/1/2且j点颜色0/1/2时的方案数*/
void dfs(ll l,ll r){ //p[l]相当于k 
	if(l+1==r) dp[l][r][0][1]=dp[l][r][1][0]=dp[l][r][2][0]=dp[l][r][0][2]=1;
	else if(p[l]==r){
		dfs(l+1,r-1);
		//01、02、10、20 
		for(int i=0;i<=2;++i)
			for(int j=0;j<=2;++j){
				if(j!=1) dp[l][r][0][1]+=dp[l+1][r-1][i][j],dp[l][r][0][1]%=mod; //傻了 
				if(j!=2) dp[l][r][0][2]+=dp[l+1][r-1][i][j],dp[l][r][0][2]%=mod;
				if(i!=1) dp[l][r][1][0]+=dp[l+1][r-1][i][j],dp[l][r][1][0]%=mod;
				if(i!=2) dp[l][r][2][0]+=dp[l+1][r-1][i][j],dp[l][r][2][0]%=mod;
			}
	}
	else{
		//处理出dp[l][p[l]]和dp[p[l]+1][r]的方案数然后相乘 
		dfs(l,p[l]),dfs(p[l]+1,r);
		for(int i=0;i<=2;++i) //l
			for(int j=0;j<=2;++j) //p[l]
				for(int k=0;k<=2;++k) //p[l]+1
					for(int kk=0;kk<=2;++kk){ //r
						if(j==k&&j!=0) continue;
						dp[l][r][i][kk]+=(dp[l][p[l]][i][j]*dp[p[l]+1][r][k][kk]%mod);
						dp[l][r][i][kk]%=mod;
					}
	}
}
int main(){
	scanf("%s",s+1); n=strlen(s+1);
	for(int i=1;i<=n;++i)
		if(s[i]=='(') st.push(i);
		else{
			p[st.top()]=i;
			p[i]=st.top();
			st.pop();
		}
	dfs(1,n); //还是得dfs来递归求解 
	ll ans=0;
	for(int i=0;i<=2;++i)
		for(int j=0;j<=2;++j)
			ans+=dp[1][n][i][j],ans%=mod;
	cout<<ans;
	return 0;
}

P1040加分二叉树

说实话区间dp是我一开始没想到的,但是毕竟不用去管树的形态,所以区间dp是完全可行的。那状态也就表示 [i,j] 建成树的最大分数,初始将 dp[i][i] 设为 a[i]。然后还珂以多整一步,将 dp[i][i1] 设为 1,因为空树用 1 表示,后面会用上。

然后就进行转移,区间长度要从 2 开始循环要不然就会重复计算了。枚举 k[i,j] 作为这棵树的根,转移式:dp[i][j]=max(dp[i][j],dp[i][k1]dp[k+1][j]+dp[k][k])

此时先前的初始化 dp[i][i1] 就用上了,防止了 k 的越界所造成的答案影响。如果不加的话则需在枚举 k 之前加上一句 dp[i][j]=dp[i+1][j]+dp[i][i] 默认 i 为根作为初值避免转移不到。

但是这道题还需要输出方案,那么就再开一个数组out记录 [i,j] 的根。初始时设成 out[i][i]=i,转移时初值设为 i 默认为根,然后赋值为 k。一遍先序遍历就出来了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145,M=1919810;
ll n,a[N],dp[N][N],out[N][N];
//表示区间[i,j]建成树的最大分数
void dfs(ll l,ll r){
	if(l>r) return;
	cout<<out[l][r]<<" ";
	if(l==r) return;
	dfs(l,out[l][r]-1);
	dfs(out[l][r]+1,r);
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],dp[i][i]=a[i],out[i][i]=i,dp[i][i-1]=1;
	for(int len=2;len<=n;++len) //len从2开始枚举 
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			//dp[i][j]=dp[i+1][j]+a[i];
			out[i][j]=i;
			for(int k=i;k<=j;++k){
				//dp[i][j]=max(dp[i][j],dp[i][k-1]*dp[k+1][j]+a[k]);
				if(dp[i][j]<dp[i][k-1]*dp[k+1][j]+dp[k][k])
					dp[i][j]=dp[i][k-1]*dp[k+1][j]+dp[k][k],out[i][j]=k;
			}
		}
	cout<<dp[1][n]<<'\n';
	dfs(1,n);
	return 0;
}

P1284三角形牧场

是不是有时候记搜是写不出来的啊?我还以为我连记搜都写不来(虽然确实难一点就写不出来)/ll

一个背包,设 dp[i][a][b] 为选了前 i 块木板,一边长度为 a,一边长度为 b 时的最大面积,还有一边可以用 sumab 算出。

其实这个没什么转移的,就是先判断 a,b,c 三边是否能被木板拼出来然后再去组合找最大值。所以转移过程中 i 只负责枚举 l[i],那么 i 这一维就珂以滚掉了。

现在是要找到能被拼出来的三边,dp主要就是搞这个的。 a,bsum/2 开始从大到小枚举。判断如下:

for(int i=1;i<=n;++i)
		for(ll a=sum/2;a>=0;--a)
			for(ll b=sum/2;b>=0;--b){
				if(a-l[i]>=0&&dp[a-l[i]][b]) dp[a][b]=1;
				if(b-l[i]>=0&&dp[a][b-l[i]]) dp[a][b]=1;
			}

然后就利用海伦公式求解面积就行了,还要判断是否任意两边大于第三边。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define db double
const ll N=45,M=1919810;
ll n,l[N],sum;
db ans;
db S(ll a,ll b,ll c){
	db p=(a+b+c)*1.0/2.0;
	return sqrt(p*1.0*(p-a)*1.0*(p-b)*1.0*(p-c));
}
bool ck(ll a,ll b,ll c){
	if(a+b<c) return 0;
	if(a+c<b) return 0;
	if(b+c<a) return 0;
	return 1;
}
db dp[805][805]; //设选了前i块木板,边a长j,边b长k,边c长sum-j-k时的最大面积
//把i滚了 
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>l[i],sum+=l[i],dp[0][0]=1;
	for(int i=1;i<=n;++i)
		for(ll a=sum/2;a>=0;--a)
			for(ll b=sum/2;b>=0;--b){
				if(a-l[i]>=0&&dp[a-l[i]][b]) dp[a][b]=1;
				if(b-l[i]>=0&&dp[a][b-l[i]]) dp[a][b]=1;
			}
	//for(int i=1;i<=n;++i)
		for(int a=sum/2;a>0;--a)
			for(int b=sum/2;b>0;--b){
				ll c=sum-a-b;
				if(!dp[a][b]||!ck(a,b,c)) continue;
				ans=max(ans,S(a,b,c));
			}
	if(ans==0) cout<<-1;
	else cout<<(ll)(ans*100);
	return 0;
}

P1387&ABC311E

都是二维dp求条件下最大矩形。有些这种类型的题还需要辅以二位前缀和。

状态:dp[i][j] 表示以 (i,j) 为右下角的满足条件的矩形的个数/边长。转移:dp[i][j]=min(dp[i1][j],dp[i][j1],dp[i1][j1])+1,前提是满足限制条件。

为了满足合法条件就只能在三者中取最小值,画个图能更好的理解。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=5005,M=1919810;
ll n,m,k;
ll f[N][N];
ll sum[N][N],dp[N][N];
int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=k;++i){
		ll x,y;
		cin>>x>>y;
		f[x][y]=1;
	}
	ll ans=0;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j){
			if(f[i][j]) continue;
			dp[i][j]=min(min(dp[i-1][j-1],dp[i][j-1]),dp[i-1][j])+1;
			ans+=dp[i][j];
            //最大正方形这里是ans=max(ans,dp[i][j]);
		}
	cout<<ans;
	return 0;
}

----------------------------------------------------------------------------------
接下来的题可能就会有些难度了,直接想不出来应该是正常的,关键在于能想到哪里。

P5851 [USACO19DEC] Greedy Pie Eaters P

区间dp,设 dp[i][j] 为吃 [i,j] 所能得到的最大体重。大概转移式是 dp[i][j]=max(dp[i][j],dp[i][k1]+dp[k+1][j]+val[k])

现在是怎么求这个 val[k],但是因为每个牛的范围都是不一样的,一个单独的 val[k] 是无法确定的,所以来重新设成 val[i][j][k] 表示 [i,j]k 位置能获得的最大体重。

这个辅助数组也可以用区间dp的方式来求解,转移:val[i][j][k]=max(max(val[i1][j][k],val[i][j+1][k]),val[i][j][k]),1ikjn。但是当 i=1j=n 时转移不能超出范围。

还有点没写完,心情不好,等之后再说吧……

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810;
ll n,m;
struct xx{
	ll val,l,r;
}w[N];
ll dp[305][305]; //设为吃了[i,j]的最大体重
ll a[305][305][305]; //设为在[i,j]中的k位置能获得的最大体重
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;++i){
		cin>>w[i].val>>w[i].l>>w[i].r;
		for(int j=w[i].l;j<=w[i].r;++j)
			a[w[i].l][w[i].r][j]=max(a[w[i].l][w[i].r][j],w[i].val);
	}
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			for(int k=i;k<=j;++k){
				if(i!=1) a[i-1][j][k]=max(a[i-1][j][k],a[i][j][k]);
				if(j!=n) a[i][j+1][k]=max(a[i][j+1][k],a[i][j][k]);
			}
		}
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			for(int k=i;k<j;++k)
				dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
			for(int k=i;k<=j;++k){
				ll x=a[i][j][k];
				if(k>i) x+=dp[i][k-1];
				if(k<j) x+=dp[k+1][j];
				dp[i][j]=max(dp[i][j],x);
			}
		}
	ll ans=0;
	for(int i=1;i<=n;++i)
		for(int j=i;j<=n;++j)
			ans=max(ans,dp[i][j]);
	cout<<ans;
	return 0;
} 

P2890 [USACO07OPEN] Cheapest Palindrome G

首先珂以发现增加和减少操作在形成回文串上其实是等价的,所以转移的时候直接取 min 就好。

对于初始化,每个点都是回文串所以 dp[i][i]=0,然后把数组其他位置都赋成极大值。

对于转移,因为回文串成中心对称,递推的话就只和端点左右的状态有关,所以不需要枚举 k,枚举了也用不上。如果两个端点相同 dp[i][j]=min(dp[i][j],dp[i+1][j1]),但是 i+1=j 时不需要进行更改操作,所以赋值为 0。式子:dp[i][j]=min(dp[i+1][j]+min(c1[s[i]],c2[s[i]]),dp[i][j1]+min(c1[s[j]],c2[s[j]])),注意要在判断端点相同之前转移,不然就错了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2001,M=1919810;
ll n,m,s[N],c1[N],c2[N];
char ch;
ll dp[N][N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;++i){
		cin>>ch;
		s[i]=ch-'a';
	}
	memset(dp,63,sizeof(dp));
	for(int i=1;i<=n;++i){
		cin>>ch;
		cin>>c1[ch-'a']>>c2[ch-'a'];
	}
	for(int i=1;i<=m;++i) dp[i][i]=0;
	for(int len=2;len<=m;++len)
		for(int i=1;i<=m-len+1;++i){
			ll j=i+len-1;
			dp[i][j]=min(dp[i+1][j]+min(c1[s[i]],c2[s[i]]),dp[i][j-1]+min(c1[s[j]],c2[s[j]]));
			if(s[i]==s[j]){
				if(j-i==1) dp[i][j]=0;
				else dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
			}
		}
	cout<<dp[1][m];
	return 0;
}

UVA1437 String painter

一眼区间dp,但是状态表示区间 [i,j] 的什么呢?直接设 A 变成 B 的最小步数好像不太方便,那么这个时候珂以转化思路,改成一个空串变成B的最小步数,看成像涂色那样做,转移式就很简单了:如果 B[i]=B[j],则向内转移一格 dp[i][j]=min(dp[i][j],dp[i+1][j1]),并且枚举 kdp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]),k[i,j)

这样子求出来之后我们再记一个 ansi 表示 [1,i] 的最小步数。还是可以用区间dp的方式来算,只是这里的左端点确定了而已。转移:如果 A[i]=B[i],则 ans[i]=min(ans[i],ans[i1]),并且枚举 jans[i]=min(ans[i],ans[j]+dp[j+1][i])

做完了,这种把复杂一点的问题转化成简单的问题再求解也是一种技巧。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=105,M=1919810,inf=1145141919;
string A,B;
ll n,a[N],b[N],dp[N][N],ans[N];
int main(){
	while(cin>>A>>B){
		n=A.size();
		memset(dp,63,sizeof(dp));
		for(int i=1;i<=n;++i){
			a[i]=A[i-1]-'a',b[i]=B[i-1]-'a';
			dp[i][i]=1;
		}
		for(int len=2;len<=n;++len) //注意区间长度从2开始枚举 
			for(int i=1;i<=n-len+1;++i){
				ll j=i+len-1;
				for(int k=i;k<j;++k)
					dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
				if(b[i]==b[j]) dp[i][j]=min(dp[i+1][j],dp[i][j-1]);
			}
		for(int i=1;i<=n;++i){
			ans[i]=dp[1][i];
			for(int j=1;j<i;++j)
				ans[i]=min(ans[i],ans[j]+dp[j+1][i]);
			if(a[i]==b[i]) ans[i]=min(ans[i],ans[i-1]);
		}
		cout<<ans[n]<<'\n';
	}
	return 0;
}

P6419 [COCI2014-2015#1] Kamp

一眼换根dp,不过就不会了。一开始还以为要树上差分,但是不行。我们先定义一些量:sizeu 为以 u 为根的子树中的人数、gu 为以 u 为起点将子树内的所有人送回家并回到 u的最小时间(这一个设得很妙,解决了我在想哪一个点结束并且还要考虑往返时间不同的问题)、lian1u,lian2u 表示以 u 为起点要到达的最远和次远的有人的点的距离(也就是最长链和次长链)、sonu 记录最长链。第一遍dfs的时候就是把这些量给求解出来。注意 g,lian1,lian2 要在该子树中有人的时候才会更新,g[u]=g[u]+2×ww 为边权)

然后是第二遍dfs,我们把dp数组设为对于整棵树从 u 开始送人并回到 u 的最短距离,我们注意到送完人珂以不回到初始点,所以最后的答案还要减去最长链的长度(智慧)。在这一遍dfs中我们便要更新dp和最长/次长链。

分类讨论三种情况:

  1. 该子树内没有人,直接加上w:dp[v]=dp[u]+2w,lian1[v]=lian1[u]+w

  2. 该子树内包含了所有的人,链长度不变,dp便是之前求出来的g:dp[v]=g[v]

  3. 以上两种情况之外的情况,画棵树模拟一下可发现 dp[v]=dp[u],但是链的更新就需要大力分讨一下,直接放个代码吧:

if(lian1[u]+w>=lian1[v]&&son[u]!=v) lian1[v]=lian1[u]+w,son[v]=u; 注意更新链的点
if(lian1[u]+w>=lian1[v]&&son[u]==v) 不可以更新
if(lian2[u]+w>=lian1[v]           ) lian1[v]=lian2[u]+w,son[v]=114514; 随便设一个
if(lian1[u]+w>=lian2[v]&&son[u]!=v) lian2[v]=lian1[u]+w;
if(lian1[u]+w>=lian2[v]&&son[u]==v) 不可以更新
if(lian2[u]+w>=lian2[v]           ) lian2[v]=lian2[u]+w;

最后答案就是 dp[u]lian1[i]。话说这种题确实思维难度高,珂以发现有时候不一定要让dp数组直接求出答案,珂以通过妙妙转化把答案转化成多个量之间的运算结果,让dp可行易解。还有其他做法似乎比这个简洁一些,不过这个确实好懂。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=5*114514,M=1919810,inf=1145141919;
struct xx{
	ll next,to,val;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
}
ll n,K; bool k[N];
ll dis[N],size[N]; //子树内的人数 
ll g[N]; //以u为起点送子树内人回家并回到u的总时间,真实答案还要减去一个子树的最长链的长度
ll lian1[N],lian2[N],son[N]; //最长链、次长链?也就是最远和次远要到达的点的距离
void dfs1(ll u,ll fa){
	if(k[u]) ++size[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		dis[v]=dis[u]+w;
		dfs1(v,u);
		size[u]+=size[v];
	}
	if(size[u]){
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to,w=e[i].val,len=lian1[v]+w;
			if(v==fa||!size[v]) continue;
			g[u]+=g[v]+2*w;
			if(len>=lian1[u]) lian2[u]=lian1[u],lian1[u]=len,son[u]=v;
			else if(len>lian2[u]) lian2[u]=len;
		}
	}
}
ll dp[N]; //对于整棵树从u开始送人并回到u的最短距离 
void dfs2(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		//分类讨论 
		if(!size[v]){
			dp[v]=dp[u]+2*w;
			lian1[v]=lian1[u]+w;
		}
		else if(K-size[v]==0) dp[v]=g[v];
		else{
			dp[v]=dp[u];
			if(lian1[u]+w>=lian1[v]&&son[u]!=v) lian1[v]=lian1[u]+w,son[v]=u;
		  //if(lian1[u]+w>=lian1[v]&&son[u]==v) 不可以更新
		    if(lian2[u]+w>=lian1[v]           ) lian1[v]=lian2[u]+w,son[v]=114514;
		    if(lian1[u]+w>=lian2[v]&&son[u]!=v) lian2[v]=lian1[u]+w;
		  //if(lian1[u]+w>=lian2[v]&&son[u]==v) 不可以更新
		    if(lian2[u]+w>=lian2[v]           ) lian2[v]=lian2[u]+w;
		}
		dfs2(v,u);
	}
}
int main(){
	cin>>n>>K;
	for(int i=1;i<n;++i){
		ll a,b,c;
		cin>>a>>b>>c;
		add(a,b,c),add(b,a,c);
	}
	for(int i=1;i<=K;++i){
		ll x; cin>>x;
		k[x]=1;
	}
	dfs1(1,0); dp[1]=g[1];
	dfs2(1,0);
	//for(int i=1;i<=n;++i) cout<<lian1[i]<<" ";cout<<'\n';
	for(int i=1;i<=n;++i) cout<<dp[i]-lian1[i]<<'\n';
	return 0;
}

P8675 [蓝桥杯 2018 国 B] 搭积木

一眼区间dp,但是这里变成三维的了,我们就设为在第 i 层的 [l,r] 区间摆积木(摆满)的方案数。初始化的话就先对每一行做一个X的前缀和方便判断一个区间里面有没有X,如果没有那么初始化为 0,否则反之。

这个时候我们珂以整一个二维前缀和来进行转移,首先对于每一行先把那一行的二维前缀和处理出来,然后再对那一行方案数统计:dp[i][l][r]=sum[i][m]sum[0][m]+sum[0][j1]sum[i][j1],而且这里是可以化简的,代码里有。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114,M=1919810,mod=1e9+7;
ll n,m,f[N][N],sum[N][N],dp[N][N][N];
//设为在第i层[l,r]摆积木的方案数
char ch; ll ans=1;
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j){
			cin>>ch;
			f[i][j]=f[i][j-1]+(ch=='X');
		}
	for(int i=1;i<=m;++i)
		for(int j=i;j<=m;++j){
			dp[n][i][j]=(f[n][j]-f[n][i-1]==0); //区间有X则初始方案为0
			ans+=dp[n][i][j],ans%=mod; 
		}
	for(int c=n-1;c>=1;--c){
		for(int i=1;i<=m;++i)
			for(int j=1;j<=m;++j)
				sum[i][j]=(dp[c+1][i][j]+sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1])%mod;
		for(int i=1;i<=m;++i)
			for(int j=i;j<=m;++j){
				if(f[c][j]-f[c][i-1]!=0) continue; //有X
				dp[c][i][j]=(sum[i][m]-sum[i][j-1])%mod; //把为0两项的两项减了 
				ans+=dp[c][i][j],ans=(ans+mod)%mod;
			}
	}
	cout<<(ans+mod)%mod;
	return 0;
}

Chain Reaction

傻逼RMJ又炸了

水题,设 dp[i] 为依次激活塔 i剩余的最多的塔数。我们可以发现一座塔是不会对他右边的塔产生影响的,所以我们珂以从位置 0 开始向右边dp,如果一座塔的威力大于等于当前位置那么 dp[i]=1,否则 dp[i]=dp[ival[i]1]+1

如果这个位置没有塔直接 dp[i]=dp[i1] 就行了,每次取最大值。注意 0 的位置要特殊判断因为他取不到前面的位置。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810,mod=1e9+7;
ll n,dp[N],maxn,ans;
/*设为依次激活塔i的最大保留数 
一座塔不会对他右边的塔产生影响*/
struct xx{
	ll a,b;
}a[N];
bool cmp(xx x,xx y){
	return x.a<y.a;
}
map <ll,bool> fl;
map <ll,ll> val; //稍大的数据用map存好些,也就多一个老哥
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>a[i].a>>a[i].b;
		maxn=max(maxn,a[i].a);
		fl[a[i].a]=1;
		val[a[i].a]=a[i].b;
	}
	sort(a+1,a+n+1,cmp); //似乎不需要排序
	if(fl[0]) dp[0]=1; //往不了前 
	for(int i=0;i<=maxn;++i){
		if(fl[i]){
			if(val[i]>=i) dp[i]=1;
			else dp[i]=dp[i-val[i]-1]+1;
		}
		else dp[i]=dp[i-1];
		ans=max(ans,dp[i]);
	}
	cout<<n-ans;
	return 0;
}

P3621 [APIO2007] 风铃

这个题比较简单,感觉不能叫dp只能叫贪心。先一遍dfs处理出每个点的子树大小以及星星的最大深度和最小深度,然后第二遍dfs,如果左儿子比右儿子的 size 小的话那么 ans 就加一。

然后是判断无解,先是如果最大深度和最小深度之间相差大于 1,那么无解;如果相等输出 0;然后多整一个fl数组记录当前点的左右儿子子树中是否都是既有 1 又有非 1 的节点,如果左右儿子子树中都有那么就输出 1

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810;
ll n,a,b,size[N],dept[N];
ll root,maxd,mind=1145141919;
bool f[N],fl[N][2];
struct tree{
	ll l,r;
}t[2*N];
void dfs1(ll u,ll dep){
	dept[u]=dep; 
	if(t[u].l==-1) ++size[u],f[dep]=1,maxd=max(maxd,dep),mind=min(mind,dep);
	if(t[u].r==-1) ++size[u],f[dep]=1,maxd=max(maxd,dep),mind=min(mind,dep);
	if(t[u].l>0) dfs1(t[u].l,dep+1);
	if(t[u].r>0) dfs1(t[u].r,dep+1);
	size[u]+=size[t[u].l]+size[t[u].r];
}
ll ans=0;
void dfs2(ll u,ll fa,ll flag){
	if(size[t[u].l]<size[t[u].r]) ++ans;
	if(t[u].l>0) dfs2(t[u].l,u,0);
	if(t[u].r>0) dfs2(t[u].r,u,1);
	if((t[u].l==-1&&t[u].r>-1)||(t[u].l>-1&&t[u].r==-1)) fl[fa][flag]=1;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>t[i].l>>t[i].r;
		f[a]=f[b]=1;
	}
	for(int i=1;i<=n;++i) //这个题都不用判断根的…… 
		if(!f[i]){
			root=i;
			break;
		}
	memset(f,0,sizeof(f));
	dfs1(root,1);
	dfs2(root,0,0);
	if(maxd-mind>1){cout<<-1; return 0;}
	else if(maxd-mind==0){cout<<0; return 0;}
	for(int i=1;i<=n;++i)
		if(fl[i][0]&&fl[i][1]){
			cout<<-1;
			return 0;
		}
	cout<<ans;
	return 0;
}

P2224 [HNOI2001] 产品加工

智慧+毒瘤卡常dp题

我们可以先从简单的状态设计开始:设 dp[i][j][k] 为dp到了第 i 件工作 A 做了 j 时间的工作 B 做了 k 时间工作时是否有这样一个节点,将答案所求设计在状态表示里。然后考虑怎么简化这个状态,考虑减少一维状态移到状态的值中,那么就珂以设 dp[i][j] 为dp到第 i 件工作 A 做了 j 时间工作时 B 做工的时间。
人有五名,转移有三个:
1.交给 Adp[i][j]=dp[i1][jt1[i]]
2.交给 Bdp[i][j]=dp[i1][j]+t2[i]
3.A,B 一起做 dp[i][j]=dp[i1][jt3[i]]+t3[i]
但是在 6e3 的数据下这个 O(n2) 做法似乎依旧会卡,那么我们继续尝试优化。我们发现这已经和01背包很像了,所以考虑把 i 的那一维给滚掉,只留下 j 这一维。但是如果将背包的重量上界每次都设为 5×n 的话总共是 3e8 级别的时间,依旧在爆炸的边缘徘徊。那我们就用一个可以说是卡常的优化技巧:设当前枚举到第 i 项工作,那么珂以将背包上界设成 m=Σk=1imax(t1[i],t3[i])

欸?我好像直接设的 t1[i]+t3[i]?怪不得被卡常了,还加了IO优化并且边读边处理才压线过了……

总结:头一次见卡常的dp,不过这个题的优化过程值得借鉴。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=114514,M=1919810,inf=1145141919;
ll n,t1[N],t2[N],t3[N];
//非常智慧的dp
//智慧!!! 
ll dp[N],m;
static char space[36000000],*sp=space;
template<typename T>
struct myalloc:allocator<T>{
    myalloc(){}
    template<typename T2>
    myalloc(const myalloc<T2> &a){}
    template<typename T2>
    myalloc<T>& operator=(const myalloc<T2> &a){return *this;}
    template<typename T2>
    struct rebind{typedef myalloc<T2> other;};
    inline T* allocate(size_t n){
        T *result=(T*)sp;sp+=n*sizeof(T);
        return result;
    }
    inline void deallocate(T* p,size_t n){}
};
template<class T>inline bool ckmin(T &x,const T &y){return x>y?x=y,1:0;}
static char pbuf[1000000],*p1=pbuf,*p2=pbuf,obuf[1000000],*o=obuf;
#define getchar() p1==p2&&(p2=(p1=pbuf)+fread(pbuf,1,1000000,stdin),p1==p2)?EOF:*p1++
#define putchar(x) (o-obuf<1000000)?(*o++=(x)):(fwrite(obuf,o-obuf,1,stdout),o=obuf,*o++=(x))
ll read(){
    ll x(0);
    char ch=getchar();
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0' && ch<='9')
        x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return x;
}
ll num[100];
inline void write(int x)
{
	if(x<0) putchar('-'),x=-x;;
	ll len=0;
	do num[len++]=x%10;while(x/=10);
	while(len--) putchar(num[len]+'0');
} 
struct flusher{~flusher(){fwrite(obuf,o-obuf,1,stdout);}}autoflush;
int main(){
	n=read();
	for(int i(1);i<=n;++i) dp[i]=inf;
	dp[0]=0;
	for(int i(1);i<=n;++i){
		t1[i]=read(),t2[i]=read(),t3[i]=read(); //妈的你还卡我常数 
		m+=t1[i]+t3[i]; //额(⊙﹏⊙) 
		for(int j(m);j>=0;--j){
			ll x=inf;
			if(j>=t1[i]) x=dp[j-t1[i]];
			ll y=dp[j]+t2[i],z=inf;
			if(j>=t3[i]) z=dp[j-t3[i]]+t3[i];
			if(!t1[i]) x=inf; //为0的时候不能取 
			if(!t2[i]) y=inf;
			if(!t3[i]) z=inf;
			dp[j]=min(x,min(y,z)); //这些转化太智慧了
		}
	}
	ll ans=inf;
	for(int i(m);i>=0;--i) ans=min(ans,max(i,dp[i]));
	write(ans);
	return 0;
}

P8816 [CSP-J 2022] 上升点列

排序写错调了半天/fn

简单dp,设 dp[i][k] 为dp到第 i 个点时还剩下 k 个可使用点时的最大长度。我们在做这一类有消耗的dp时设计的状态一般都是还剩下多少个而不是用了多少个,这是我们关心的而且方便转移。

然后枚举前面的点转移,两个点的距离定义为 |xixj|+|yiyj|1,如果前面的点的横或纵坐标大于当前点那就不可转移。然后 dp[i][k]=max(dp[j][k+dis]+dis+1),注意加上 j 本身的 1

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=514,M=1919810;
ll n,m;
struct xx{
	ll x,y;
}a[N];
bool cmp(xx x,xx y){
	return x.x==y.x?x.y<y.y:x.x<y.x;
	//淦原来写错了
}
ll dp[N][N]; //设为dp到第i个点并且剩余k个点时的LIS
//连接所需点数(x1-x2)+(y1-y2)-1
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>a[i].x>>a[i].y;
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;++i) dp[i][m]=1; 
	for(int i=1;i<=n;++i)
		for(int k=0;k<=m;++k)
			for(int j=1;j<i;++j){
				if(a[j].x>a[i].x||a[j].y>a[i].y) continue;
				ll delta=abs(a[i].x-a[j].x)+abs(a[i].y-a[j].y)-1;
				if(k+delta>m) continue;
				dp[i][k]=max(dp[i][k],dp[j][k+delta]+delta+1);
			}
	ll ans=0;
	for(int i=1;i<=n;++i)
		for(int k=0;k<=m;++k)
			ans=max(ans,k+dp[i][k]);
	cout<<ans;
	return 0;
}

P7914 [CSP-S 2021] 括号序列

遇到这种需要dp的东西是不确定的,要求我们记录方案数的情况时珂以考虑对状态进行分类讨论,用尽量简洁并且能转移的分类来概括状态。
傻逼Markdown打不出来连续的星号,截张图讨论:
piP7jsS.png
然后我们就按着这个思路去转移,注意有连续星号不能超过 k 个的限制,所以我们要注意在对以星号结尾和开头的段转移时判断总个数是否小于等于 k
写一下转移过程:

  • dp[i][j][0]=dp[i][j1][0](ji+1k,s[j]=?s[j]=)

  • dp[i][j][1]=dp[i+1][j1][0]+dp[i+1][j1][2]+dp[i+1][j1][3]+dp[i+1][j1][4],如题面所说

  • dp[i][j][2]=Σk=ij1dp[i][k][3]dp[k+1][j][0]

  • dp[i][j][3]=Σk=ij1(dp[i][k][2]+dp[i][k][3])dp[k+1][j][1]+dp[i][j][1]

  • dp[i][j][4]=Σk=ij1(dp[i][k][4]+dp[i][k][5])dp[k+1][j][1]

  • dp[i][j][5]=Σk=ij1dp[i][k][4]dp[k+1][j][0]+dp[i][j][0]

具体过程之后补,虽然可能不记得了……

LaTeX 的 tag

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=505,M=1919810,mod=1e9+7;
ll n,K,dp[N][N][7];
char s[N];
/*1.形如************
2.形如(............)
3.形如(.......)*****
4.形如(...)****(...)
5.形如*****(.......)
6.形如*(...)**(...)*
*/
int main(){
	cin>>n>>K>>(s+1);
	for(int i=1;i<=n;++i) dp[i][i-1][0]=1; //反正要越界,不如初始化越界 
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			if(len<=K) dp[i][j][0]=dp[i][j-1][0]&&(s[j]=='*'||s[j]=='?');
			if(len>1){
				if((s[i]=='('||s[i]=='?')&&(s[j]==')'||s[j]=='?')) //我操操操操操擦
				//妈的改了一个小时 
					dp[i][j][1]=dp[i+1][j-1][0]+dp[i+1][j-1][2]+dp[i+1][j-1][3]+dp[i+1][j-1][4],dp[i][j][1]%=mod;
				for(int k=i;k<j;++k){
					dp[i][j][2]+=dp[i][k][3]*dp[k+1][j][0],dp[i][j][2]%=mod;
					dp[i][j][3]+=(dp[i][k][2]+dp[i][k][3])*dp[k+1][j][1],dp[i][j][3]%=mod;
					dp[i][j][4]+=(dp[i][k][4]+dp[i][k][5])*dp[k+1][j][1],dp[i][j][4]%=mod;
					dp[i][j][5]+=dp[i][k][4]*dp[k+1][j][0],dp[i][j][5]%=mod;
				}
			}
			dp[i][j][3]+=dp[i][j][1],dp[i][j][3]%=mod;
			dp[i][j][5]+=dp[i][j][0],dp[i][j][5]%=mod;
		}
	cout<<dp[1][n][3]; //符合要求的情况 
	return 0;
}

Tree with Maximum Cost

之前做的换根dp基础题的双倍经验,就当是巩固寄础了。这个题的话我们先把以 1 为根时到每个点的距离代价并且把这个点的子树大小(加上点权)处理出来。第二遍换根,dp[v]=dp[u]size[v]+(sumsize[v]),dp[1]=dis[1]

搬的code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810,inf=1145141919810;
ll n,x,y,z;
struct xx{
	ll next,to,val;
}e[2*N];
ll head[2*N],cnt,sum; 
ll c[N],size[N],dis[N],dp[N],ans=-inf;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
}
ll dfs(ll u,ll fa){
	ll tot=0;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		ll s=dfs(v,u);
		dis[u]+=dis[v]+s*w;
		tot+=s;
	}
	return size[u]=c[u]+tot;
}
void dfs2(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		dp[v]=dp[u]-size[v]*w+(sum-size[v])*w;
		dfs2(v,u);
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>c[i],sum+=c[i];
	for(int i=1;i<n;++i){
		cin>>x>>y;
		add(x,y,1);
		add(y,x,1);
	}
	dfs(1,0);
	dfs2(1,0);
	for(int i=1;i<=n;++i)
		ans=max(dp[i],ans);
	cout<<ans+dis[1];
	return 0;
}

P5666 [CSP-S2019] 树的重心

妈的今天必须把这个题搞出来。

我们可以先发掘几条性质:

  • 一棵树的重心有一个或两个,若有两个他们必定相邻。我们在下面求出的重心一定是深度较大的那一个。

  • 一棵树的重心必定在以这个树根为起点的重链上。

  • 一棵树的重心必定是以/这棵树的根的重儿子/为根/的子树/的重心/的祖先(有点绕,但确是如此)

那我们就珂以把重心当作树的根,根据性质3预处理出所有子树的重心,从叶往根处理,时间 O(n)

每当我们删去一条边后,树会分成两个部分,设深度小的点为 x,深度大的点为 yy 部分的答案就是预处理出来的重心,x 部分的答案需要分两类:

  • y 不在根的重儿子子树内:这样子我们根据性质2就可以沿着根的重链,根据子树的大小去把答案预处理出来,也是 O(n)

  • y 在根的重儿子的子数内:我们可以照着上一种情况把在根的次重儿子所在的重链的重心预处理出来。

然后在统计答案的时候还要判断我们求出来的重心的父亲是不是也是重心,根据是哪一种情况来变化。

结果写完了才发现没用到dp,放到树论那边去了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3*114514,M=1919810;
ll T,n,a[N],b[N];
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll size[N],son[N],f[N],dept[N],son_t[N]; //属于根的哪个子树 
ll root,leson; //整棵树重心和根的次重儿子 
void dfs_pre(ll u,ll fa){
	size[u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_pre(v,u);
		size[u]+=size[v];
		if(size[son[u]]<size[v]) son[u]=v;
	}
	if(size[son[u]]*2<=n&&(n-size[u])*2<=n) root=u;
}
void dfs_son(ll u,ll fa){
	size[u]=1; dept[u]=dept[fa]+1; f[u]=fa;
	if(fa==root) son_t[u]=u;
	else son_t[u]=son_t[fa];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_son(v,u);
		size[u]+=size[v];
		if(size[son[u]]<size[v]){
			if(u==root) leson=son[u];
			son[u]=v;
		}
		else if(u==root&&size[v]>size[leson]) leson=v;
	}
}
ll ans1[N],ans2[N],ans3[N],tot;
void dfs_ans1(ll u,ll fa){ //直接求某子树的重心 
	if(son[u]) dfs_ans1(son[u],u);
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa||v==son[u]) continue;
		dfs_ans1(v,u);
	}
	ll now=son[u]?ans1[son[u]]:u; //从重心往上找
	while(size[now]*2<size[u]) now=f[now];
	ans1[u]=now; 
}
void dfs_ans2(ll u,ll fa){
	if(son[u]) dfs_ans2(son[u],u); //都走重儿子 
	while(n-2*size[u]<=tot&&tot>0){
		ans2[tot]=u;
		--tot;
	}
}
void dfs_ans3(ll u,ll fa){
	if(u==root) tot=n,dfs_ans3(leson,u); //根节点走次重儿子 
	else if(son[u]) dfs_ans3(son[u],u); //其他走重儿子
	while(n-2*size[u]<=tot&&tot>0){
		ans3[tot]=u;
		--tot;
	}
}
//三个ck判断是否有两个重心 
bool ck1(ll x,ll y){ //第一种他的父亲是不是重心 
	return x&&size[son[x]]*2<=size[y]&&(size[y]-size[x])*2<=size[y];
}
bool ck2(ll x,ll y){ //第二种父亲是不是重心 
	if(x==root){
		if(size[son[x]]*2<=n-size[y]) return 1;
		return 0; 
	}
	return x&&size[son[x]]*2<=n-size[y]&&(n-size[x]-size[y])*2<=n-size[y];
} 
bool ck3(ll x,ll y){ //第三种父亲是不是重心 
	if(x==root){
		if(size[leson]*2<=n-size[y]) return 1;
		//判的是次重儿子 
		else return 0;
	}
	return x&&size[son[x]]*2<=n-size[y]&&(n-size[x]-size[y])*2<=n-size[y];
}
void solve(){
	cin>>n;
	for(int i=1;i<=2*n;++i) head[i]=0,son[i]=0;
	tot=n,cnt=leson=0;
	for(int i=1;i<n;++i){
		cin>>a[i]>>b[i];
		add(a[i],b[i]),add(b[i],a[i]);
	}
	dfs_pre(1,0);
	for(int i=1;i<=n;++i) son[i]=0;
	dfs_son(root,0);
	dfs_ans1(root,0);
	dfs_ans2(root,0);
	dfs_ans3(root,0);
	ll ans=0;
	for(int i=1;i<n;++i){
		ll x=a[i],y=b[i];
		if(dept[x]>dept[y]) swap(x,y);
		ll pos1=ans1[y];
		ans+=pos1;
		if(dept[f[pos1]]>=dept[y]&&ck1(f[pos1],y)) ans+=f[pos1];
		if(son_t[y]!=son[root]){ //不在根的重儿子子树内
			ll pos2=ans2[size[y]];
			ans+=pos2;
			if(ck2(f[pos2],y)) ans+=f[pos2];
		}
		else{
			if(size[son[root]]-size[y]>=size[leson]) ans+=root; //根还是为重心
			else{
				ll pos3=ans3[size[y]];
				ans+=pos3;
				if(ck3(f[pos3],y)) ans+=f[pos3];
			} 
		}
	}
	cout<<ans<<'\n';
}
int main(){
	cin>>T;
	while(T--) solve();
	return 0;
}

切蛋糕 Cake slicing

二维区间dp。设状态 dp[a][b][c][d] 为以 (a,b) 为左下角,(c,d) 为右上角的矩形的最小割。先预处理二维前缀和,初始都设成极大值,然后 O(n5) 转移。

如果这个矩形内没有标记点那么不合法直接返回,如果只有一个点那么不需要画割线,答案为 0,否则按区间dp的方式进行转移,分成横着切和竖着切两种情况转移,注意横着切加的是矩形的长,竖着切加的是矩形的宽。还有些细节在 code 里。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=25;
ll n,m,k,dp[M][M][M][M],f[M][M],sum[M][M],tot;
//表示左上顶点(a,b)右下顶点(c,d)的矩形的最小割
void solve(){
	for(int len1=1;len1<=n;++len1) //区间dp注意转移顺序,区间从小到大 
		for(int len2=1;len2<=m;++len2)
			for(int a=1;a<=n-len1+1;++a)
				for(int b=1;b<=m-len2+1;++b){
					ll c=a+len1-1,d=b+len2-1;
					ll num=sum[c][d]-sum[a-1][d]-sum[c][b-1]+sum[a-1][b-1];
					if(!num) continue;
					if(num==1) dp[a][b][c][d]=0;
					else{
						for(int i=a;i<c;++i) //横着切 
							dp[a][b][c][d]=min(dp[a][b][c][d],dp[a][b][i][d]+dp[i+1][b][c][d]+len2);
						for(int i=b;i<d;++i) //竖着切 
							dp[a][b][c][d]=min(dp[a][b][c][d],dp[a][b][c][i]+dp[a][i+1][c][d]+len1);
					}
				}
}
int main(){
	while(cin>>n>>m>>k){
		memset(f,0,sizeof(f));
		memset(sum,0,sizeof(sum));
		memset(dp,63,sizeof(dp));
		for(int i=1;i<=k;++i){
			ll u,v; cin>>u>>v;
			f[u][v]=1;
		}
		for(int i=1;i<=n;++i)
			for(int j=1;j<=m;++j)
				sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+f[i][j];
		solve();
		cout<<"Case "<<++tot<<": "<<dp[1][1][n][m]<<'\n'; //草怎么还有一个空格
	}
	return 0;
}

P1453 城市环路

不难,重在转化。本来我们是当作一个基环树dp来做,但是我们可以把多出来的那条边当作一个不能同时选 s,t 的额外限制条件,然后这样就可以直接正常地dp了,最后比较各从 s,t 开始选的答案,即必须选 s/t 的情况。

dp[u][0/1] 表示不选/选,转移代码里有。还有就是注意判断这个额外的限制条件以及找到 s,t,以及我们取的答案是 dp[s/t][0]。感性理解一下可以发现确实要取不选的情况,要不然你还是有可能会 s,t都选。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define db double
const ll N=114514,M=1919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,s,t; //当作一个额外限制条件 
db k,p[N],ans,dp[N][2]; //选或不选这个点
bool vis[N];
void dfs_pre(ll u,ll fa){
	vis[u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		if(vis[v]){
			s=u,t=v;
			continue;
		}
		dfs_pre(v,u);
	}
}
void dfs_dp(ll u,ll fa){
	dp[u][1]=p[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa||(u==t&&v==s)||(u==s&&v==t)) continue;
		dfs_dp(v,u);
		dp[u][1]+=dp[v][0]; //选这个点
		dp[u][0]+=max(dp[v][0],dp[v][1]); //不选 
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>p[i];
	for(int i=1;i<=n;++i){
		ll u,v;
		cin>>u>>v; ++u,++v;
		add(u,v),add(v,u);
	}
	cin>>k;
	dfs_pre(1,0);
	dfs_dp(s,0); ans=dp[s][0]; //感性理解一下确实应该看不选的情况 
	memset(dp,0,sizeof(dp));
	dfs_dp(t,0);
	printf("%0.1lf",(db)max(dp[t][0],ans)*1.0*k);
	return 0;
}

P2607 [ZJOI2008] 骑士

和上一道题类似,但是注意图变成了基环树森林,而且有可能存在只有两个点的基环树,这个时候判断条件就要改一下,我们不去判断点而是去判断边,这样可以保证不会出现一个点都不选的情况。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define db double
const ll N=1145140,M=1919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt=1;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,x,s,t;
ll p[N],ans,res,dp[N][2];
ll siz,e1,e2;
bool vis[N];
void dfs_pre(ll u,ll fa){
	vis[u]=1; ++siz;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		if(vis[v]){
			s=u,t=v;
			e1=i,e2=i^1;
			continue;
		}
		dfs_pre(v,u);
	}
}
void dfs_dp(ll u,ll fa){
	dp[u][1]=p[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa||i==e1||i==e2) continue;
		dfs_dp(v,u);
		dp[u][1]+=dp[v][0];
		dp[u][0]+=max(dp[v][0],dp[v][1]);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>p[i]>>x;
		add(i,x),add(x,i);
	}
	for(int i=1;i<=n;++i){ //还是O(n) 
		if(vis[i]) continue;
		siz=0;
		dfs_pre(i,0);
		if(siz==2){
			int v=e[head[i]].to;
			ans+=max(p[i],p[v]);
			continue;
		}
		memset(dp,0,sizeof(dp));
		dfs_dp(s,0); res=dp[s][0];
		memset(dp,0,sizeof(dp));
		dfs_dp(t,0);
		ans+=max(res,dp[t][0]);
	}
	cout<<ans;
	return 0;
}

P4095 [HEOI2013] Eden 的新背包问题

首先是多重背包的二进制拆分,这个之前不会,但是挺简单的,就当个板子了。

然后我们看这个时间复杂度。多半是要预处理然后单次最多 O(m) 查询。我们考虑有什么状态是不包含物品 i 的,当然是 dp[i1][j],但是直接取这个的话 i 之后的物品是没有计算入内的。所以我们可以考虑dp两次,正反各一次,然后答案就是 dp[i][weight]+dp[i+1][mweight],但是因为我们将一个物品拆成了多个物品,所以这里还多一步找到这个被删去的物品的过程,看代码。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1000;
ll n,q,m,cnt;
ll dp1[N][M+5],dp2[N][M+5]; //我们关心是哪个物品,开二维
struct xx{
	ll val,id;
}w[N],v[N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		ll tmp=1,wei,val,c;
		cin>>wei>>val>>c;
		while(c){
			w[++cnt].val=tmp*wei,w[cnt].id=i; //当板子记下来
			v[cnt].val=tmp*val,v[cnt].id=i;
			c-=tmp,tmp*=2;
			if(c<tmp){
				w[++cnt].val=wei*c,w[cnt].id=i;
				v[cnt].val=val*c,v[cnt].id=i;
				break;
			}
		}
	}
	n=cnt;
	for(int i=1;i<=n;++i){
		for(int j=0;j<=M;++j) dp1[i][j]=dp1[i-1][j];
		for(int j=M;j>=w[i].val;--j)
			dp1[i][j]=max(dp1[i][j],dp1[i-1][j-w[i].val]+v[i].val);
	}
	for(int i=n;i>=1;--i){ //反向搞一次 
		for(int j=0;j<=M;++j) dp2[i][j]=dp2[i+1][j];
		for(int j=M;j>=w[i].val;--j)
			dp2[i][j]=max(dp2[i][j],dp2[i+1][j-w[i].val]+v[i].val);
	}
	cin>>q;
	while(q--){
		ll x,ans=0,l=0,r=0;
		cin>>x>>m; ++x;
		//找到被删去的物品 
		while(w[l+1].id<x&&l<n) ++l;
		r=l;
		while(w[r+1].id<=x&&r<n) ++r;
		for(int i=0;i<=m;++i)
			ans=max(ans,dp1[l][i]+dp2[r+1][m-i]); //两个部分拼起来
		cout<<ans<<'\n'; 
	}
	return 0;
}

[ABC223G] Vertex Deletion

智慧题,难。

首先对于树的最大匹配,珂以贪心地来求:从叶节点开始向上直接匹配当前点和他的父亲,然后删去这两个点,直至只剩一个点或者空树。

我们可以将这过程看作是染色,初始时全为白点,然后匹配的点染黑。可以发现这样去染一定不会有同为白色的相邻点,我们最后涂完整棵树,对于根节点如果他仍然是白色那么他就是一个可删去的点。

对于其他的点,考虑通过换根dp来求解。具体而言,我们先预处理出每个点通过我们的贪心策略能和他的几个子节点匹配,然后设一个 c[u] 表示 u 的根时是否可以被删去,如果把这个点删去后他原来的父亲能找到别的能匹配的点,那么这个点就是可以删去的,c[v]=(!c[u]||dp[u]!dp[v])
然后RMJ炸了……

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,dp[N],c[N],ans;
void dfs_pre(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_pre(v,u);
		dp[u]+=!dp[v]; //可以与此点形成匹配的点数
	}
}
void dfs_dp(ll u,ll fa){
	ans+=(c[u]&&!dp[u]);
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		c[v]=(!c[u]||dp[u]-!dp[v]);
		dfs_dp(v,u);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<n;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	dfs_pre(1,0);
	c[1]=1;
	dfs_dp(1,0);
	cout<<ans;
	return 0;
} 

P3523 [POI2011] DYN-Dynamite

智慧题

首先看到最小的最大值这种形式的答案,考虑二分答案,然后check的时候转化成最小点覆盖问题,如果覆盖点数小于等于 m 那么就合法。

对于这一类最小点覆盖问题一般是有一个dp思路和贪心思路,贪心思路是每次找深度最大的未被覆盖的点然后去覆盖 k 距离以内的点,用一个优先队列维护未被覆盖的点。这里我们考虑dp做法,设两个数组 f[u],g[u] 各为以 u 为根的子树中最远的未被覆盖的点的距离、子树中已选择的点到当前点的最小距离。初值设为 inf,inf,转移:f[u]=max(f[u],f[v]+1),g[u]=min(g[u],g[v]+1)

然后统计答案的时候要分类讨论一下,

这个最小点覆盖的代码应该是通用的,贪心要更简单些,但是以前写过了这次换一种。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define in inline
const ll N=3*100001,M=1919810,inf=1145141919;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,m,c[N],res,k;
ll d[N],p[N];//子树中最远的未被覆盖的点的距离、子树中已选择的点到当前点的最小距离
//好像最小点覆盖的dp做法都是这个形式
void dfs(ll u,ll fa){
	d[u]=-inf,p[u]=inf;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		d[u]=max(d[u],d[v]+1);
		p[u]=min(p[u],p[v]+1);
	}
	if(d[u]+p[u]<=k) d[u]=-inf;
	if(c[u]&&p[u]>k) d[u]=max(d[u],0ll);
	if(d[u]==k) ++res,d[u]=-inf,p[u]=0;
}
bool check(ll mid){
	k=mid,res=0;
	dfs(1,0);
	if(d[1]>=0) ++res;
	return res<=m;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>c[i];
	for(int i=1;i<n;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	ll l=0,r=n,ans=0;
	while(l<=r){
		ll mid=l+r>>1;
		if(check(mid)) r=mid-1,ans=mid;
		else l=mid+1;
	}
	cout<<ans;
	return 0;
} 

P2851 [USACO06DEC] The Fewest Coins G

想不出来。

我们先对这个多重背包进行二进制拆分,把硬币个数看成价值,把硬币面值看作重量,然后对于找零的人他是一个完全背包,设给的钱为 x 那么答案就是 dp1[x]+dp2[xT]。现在的问题是怎么求出这个 x,猜测这个值为 T+maxn,其中 maxn=max(wi2),i[1,n]。我们分别对重量上限为 maxn,T+maxn 做一次完全背包(找零)和多重背包(给钱)。然后对于 i[T,T+maxn]dp1[i]+dp2[iT] 取最小值就行了。

然后是为什么 xT+maxn 就行了,大概的证明就是这样的:

但是实际上我发现我们珂以投机取巧一点,通过计算这道题能留给我们的时间复杂度来确定这个 x 的取值范围,发现直接取最大数据 1202 就已经能通过了!这种投机取巧的方法也不失为一种技巧。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=11451,M=1919810,inf=1145141919810;
ll n,T,w[N],v[N],ww[N],c[N],cnt;
ll dp1[M],dp2[M],maxn,sum;
int main(){
	cin>>n>>T;
	for(int i=1;i<=n;++i){
		cin>>ww[i];
		maxn=max(maxn,ww[i]*ww[i]);
	}
	for(int i=1;i<=n;++i){
		cin>>c[i];
		sum+=ww[i]*c[i];
	}
	if(sum<T){ //特判 
		cout<<-1;
		return 0;
	}
	memset(dp1,63,sizeof(dp1));
	memset(dp2,63,sizeof(dp2));
	dp1[0]=dp2[0]=0;
	for(int i=1;i<=n;++i)
		for(int j=ww[i];j<=maxn;++j)
			dp2[j]=min(dp2[j],dp2[j-ww[i]]+1); //找零的完全背包 
	for(int i=1;i<=n;++i){
		ll tmp=1;
		while(c[i]){
			v[++cnt]=tmp;
			w[cnt]=tmp*ww[i];
			c[i]-=tmp,tmp*=2;
			if(c[i]<tmp){
				v[++cnt]=c[i];
				w[cnt]=ww[i]*c[i];
				break;
			}
		}
	}
	n=cnt;
	for(int i=1;i<=n;++i)
		for(int j=T+maxn;j>=w[i];--j)
			dp1[j]=min(dp1[j],dp1[j-w[i]]+v[i]); //给钱的多重背包
	ll ans=inf;
	for(int i=T;i<=T+maxn;++i) ans=min(ans,dp1[i]+dp2[i-T]);
	if(ans==inf) ans=-1;
	cout<<ans;
	return 0;
}

P2679 [NOIP2015 提高组] 子串

一眼做不出来,废了,还花了好久的时间,这么下去就要真寄了。

首先设计状态,时间允许我们设一个 O(nmk) 的状态,那我们就设 dp[i][j][k] 表示匹配到 A 的 第 i 个字符,B 的第 j 个字符并且将 A 分成了 k 个子串的方案数,我们再设一个类似前缀和的东西 sum 表示当前状态方案的总和。一个朴素的转移是每次去枚举上一个子串的断点,这样时间复杂度是 O(nm2k) 的:dp[i][j][k]+=sum[il1][jl1][k1],Ail=Bjl,l[0,min(i,j))sum[i][j][k]=sum[i1][j][k]+dp[i][j][k]。有70pts,不错了。

然后考虑优化,我们肯定是去想怎么去掉枚举断点 l 这一过程的。发现其实根据一般线性dp的转移形式,当前状态是可以直接从上一个状态(i1)转移来的,那么就直接是 dp[i][j][k]=dp[i1][j1][k]+sum[i1][j1][k1],Ai=Bj,这样子我们就将时间优化至 O(nmk) 了。接着考虑优化空间,发现 i 这一维是可以滚掉的,但是枚举顺序就要变成从大到小了,滚动数组优化一般都要注意枚举顺序的变化,最后答案就是 sum[m][K]

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145,M=1919810,mod=1e9+7;
ll n,m,K,dp[201][201]; //设dp到A的第i个字符,B的第j个字符取了A中k个子串的方案数
ll sum[201][201]; 
char a[N],b[N];
int main(){
	cin>>n>>m>>K;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=m;++i) cin>>b[i];
	if(n<m){cout<<0;return 0;}
	sum[0][0]=1;
	for(int i=1;i<=n;++i)
		for(int j=min(i*1ll,m);j>=1;--j)
			for(int k=min(j*1ll,K);k>=1;--k){
				if(a[i]==b[j]) dp[j][k]=dp[j-1][k]+sum[j-1][k-1];   
				else dp[j][k]=0; dp[j][k]%=mod;
				sum[j][k]=(sum[j][k]+dp[j][k])%mod;
			}
	cout<<sum[m][K];
	return 0;
}

P3052 [USACO12MAR] Cows in a Skyscraper G

能被搜索水过,但是我是来练dp的,而且这是这篇文章里的第一个状压dp。

我们考虑状压奶牛的分组情况来描述每一组的状态,设 dp[i][j] 为第 i 组的状态为 j 时这一组的重量。枚举奶牛 k,如果当前组能放进去的话就取最小值:dp[i][j|(1<<k1)]=min(dp[i][j|(1<<k1)],dp[i][j]+w[k]),否则考虑放到下一组:dp[i+1][j|(1<<k1)]=min(dp[i][j|(1<<k1)],w[k])。还有数组记得开大一点,inf 开大一点。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1<<18,mod=1e9+7;
ll n,W,w[N],ans=mod,lim,inf=1145141919810114514;
ll dp[24][M+5]; //表示第i组里面二进制状态为j时的重量 
int main(){
	cin>>n>>W;
	for(int i=1;i<=n;++i) cin>>w[i];
	lim=1<<n;
	for(int i=1;i<=n;++i)
		for(int j=0;j<lim;++j)
			dp[i][j]=inf; //我傻了 
	for(int i=1;i<=n;++i) dp[i][1<<(i-1)]=w[i]; //边界 
	for(int i=1;i<=n;++i)
		for(int j=0;j<lim;++j){
			if(dp[i][j]==inf) continue;
			for(int k=1;k<=n;++k){
				if(j&(1<<(k-1))) continue; //表示在j里面
				if(dp[i][j]+w[k]<=W)
					dp[i][j|(1<<(k-1))]=min(dp[i][j|(1<<(k-1))],dp[i][j]+w[k]);
				else
					dp[i+1][j|(1<<(k-1))]=min(dp[i][j|(1<<(k-1))],w[k]); //考虑放到下一组
			}
		}
	for(int i=1;i<=n;++i)
		if(dp[i][lim-1]!=inf){
			cout<<i;
			return 0;
		}
	return 0;
}

D. Decinc Dividing

解法很多,这里放dp方法。

我们设状态 dp[i][0/1] 表示第 i 个数放在一个单增/减序列里时这个序列末尾元素的最大/小值,如果 dp[i][0]inf,dp[i][1]inf 的话(inf 是我们设的初值),就代表这一个子序列是合法的。转移的话我们珂以去枚举每个区间,判断的话就是一串分类讨论(见代码),时间复杂度 O(n3)

然后我们考虑优化,没必要把线性dp整成一个区间dp的形式,我们考虑将枚举左端点,然后直接依次向右扩展,这样子省去了枚举右端点的时间,时间复杂度 O(n2),还得继续优化。我们将左端点从大到小枚举,我们可以用一种类似记忆化的形式,如果我们在某个点扩展到的答案并没有改变,那么再往后的答案也不会改变,那么我们就直接跳出并且使用之前记录的最右端点来更新 ans,如果当前点更新不了了,我们更新最远点为当前点然后退出,这看起来只是一个常数优化,但是他直接把我们的程序优化到了 O(n) 级别,这个证明也神奇:

这题思维难度还是挺大的,特别是设状态(用了之前提到的技巧)和最后那一步优化,有时候这种关键优化就真的只能看运气能不能蒙出来了。

code

点击查看代码
ll n,a[N],last,ans,dp[N][2];
//第i个数放在单增/减序列时序列结尾的最大/小值,如果有值那么这一段就是合法的 
void solve(ll id){
	dp[id][0]=inf,dp[id][1]=-inf; //这里特别注意一下,我们用的是填表法
	for(int i=id;i<n;++i){
		ll w0=dp[i+1][0],w1=dp[i+1][1];
		dp[i+1][0]=-inf,dp[i+1][1]=inf;
		if(a[i]<a[i+1]) dp[i+1][0]=max(dp[i][0],dp[i+1][0]);
		if(a[i]>a[i+1]) dp[i+1][1]=min(dp[i][1],dp[i+1][1]);
		if(dp[i][1]<a[i+1]) dp[i+1][0]=max(a[i],dp[i+1][0]);
		if(dp[i][0]>a[i+1]) dp[i+1][1]=min(a[i],dp[i+1][1]);
		if(dp[i+1][0]==w0&&dp[i+1][1]==w1) break; //没扩展到直接退出 
		if(dp[i+1][0]==-inf&&dp[i+1][1]==inf){last=i;break;} //更新最右边指针 
	}
	//反正这优化就挺神奇的
	ans+=last-id+1;
}
int main(){
	cin>>n; last=n;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		dp[i][0]=-inf,dp[i][1]=inf;
	}
	for(int i=n;i>=1;--i) solve(i);
	//左端点从大到小向右扩展,O(n^2)
	cout<<ans; 
	return 0;
}

G. Two Merged Sequences

是上一道题的子问题,不过要输出方案就很烦,我们珂以在dp过程中直接记录是单增还是单减序列的就行了,还有他妈的脑子抽了,最后判断 n 的时候 inf 没写负号,调了将近半个小时,淦。

code

点击查看代码
ll n,a[N],last,ans,res[N];
struct xx{
	ll val,fl;
	bool operator <(const xx &lxl)const{
		return val<lxl.val;
	}
}dp[N][2];
//烦死了还要输出方案 
int main(){
	cin>>n; last=n;
	for(int i=1;i<=n;++i) cin>>a[i];
	dp[1][0].val=inf,dp[1][1].val=-inf;
	for(int i=1;i<n;++i){
		dp[i+1][0].val=-inf,dp[i+1][1].val=inf;
		if(a[i]<a[i+1]) dp[i+1][0]=(xx){dp[i][0].val,0};
		if(a[i]>a[i+1]) dp[i+1][1]=(xx){dp[i][1].val,1};
		if(dp[i][1].val<a[i+1]) dp[i+1][0]=max((xx){a[i],1},dp[i+1][0]);
		if(dp[i][0].val>a[i+1]) dp[i+1][1]=min((xx){a[i],0},dp[i+1][1]);
	}
	if(dp[n][0].val!=-inf||dp[n][1].val!=inf){
		cout<<"YES\n";
		res[n]=(dp[n][0].val!=-inf?0:1); //整一个辅助记录数组
		for(int i=n;i>1;--i) res[i-1]=dp[i][res[i]].fl;
		for(int i=1;i<=n;++i) cout<<res[i]<<" ";
	}
	else cout<<"NO";
	return 0;
}

P3155 [CQOI2009] 叶子的染色

模拟赛做的,忘记写了,现在补一下。

这个题非常的生草,只给一个啥都反映不了的小样例,随便乱推的贪心也能过,结果只有20pts。正解当然是树形dp,我们设 dp[u][0/1]u 点染成黑色/白色时他的子树中的最少着色点数。对于有颜色要求的叶子我们将初值赋为 dp[u][k]=1,dp[u][!k]=infk 为要求颜色),其他的点处置都赋成 1。然后转移其实比较简单 dp[u][0]+=min(dp[v][0]1,dp[v][1]),dp[u][1]+=min(dp[v][0],dp[v][1]1)

但是这道题的根是不定的,我们猜想是不是要用换根dp,但是在手玩了几棵树之后我们发现不管选哪个点当根(当然不是给出的叶子),答案都是一样的!感性理解一下(淦我无法理解,反正就是猜出来的结论,也是对的),感觉题解的证明也很不明白,反正遇到这种题多手玩一下就行了。还有好像这个题硬要用换根也行。

code

点击查看代码
ll n,m,c[N],dp[N][2];
void dfs(ll u,ll fa){
	if(u<=n) return;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][0]-1,dp[v][1]);
		dp[u][1]+=min(dp[v][0],dp[v][1]-1);
	}
}
int main(){
	cin>>m>>n;
	for(int i=1;i<=n;++i){
		cin>>c[i];
		dp[i][c[i]]=1,dp[i][c[i]^1]=inf;
	}
	for(int i=1;i<m;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	for(int i=n+1;i<=m;++i) dp[i][0]=dp[i][1]=1;
	dfs(n+1,0); //随便选一个根
	cout<<min(dp[n+1][0],dp[n+1][1]);
	return 0;
}

P5999 [CEOI2016] kangaroo

这个dp比较特殊,看着像区间dp但是区间又是不太固定的,我们把它称作连续段dp(或者插入dp)。
珂以转化一下题意,我们需要求的是多少个排列 p 满足 i(1,n)pi 两边的数同时大于或小于 pi,且 p1=s,pn=t。考虑使用连续段dp求解。

dp[i][j] 表示前 i 个数划分为 j 个连续段时的方案数。

妈的先把code放这里

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*1145,M=1919810,mod=1e9+7;
ll n,s,t,dp[N][N];
int main(){
	cin>>n>>s>>t;
	dp[1][1]=1;
	for(int i=2;i<=n;++i)
		for(int j=1;j<=i;++j)
			if(i==s||i==t) (dp[i][j]=dp[i-1][j-1]+dp[i-1][j])%mod;
			else{
				dp[i][j]=j*dp[i-1][j+1]%mod; //两个连续段接起来 
				//新开一个连续段
				ll f1=(i>s),f2=(i>t);
				(dp[i][j]+=(j-f1-f2)*dp[i-1][j-1]%mod)%mod;
			}
	cout<<dp[n][1];
	return 0;
} 

[ABC264Ex] Perfect Binary Tree

没保存但是电脑关机了,重写(
首先这个题没啥性质好说,只有一个深度大于 log2n 的点不需要算的性质。我们设 dp[u][i] 为以 u 点为跟,高度为 i 的子树是满二叉的方案数。注意到这个题是要求我们一个一个把点添加进去,我们可以在添加的过程中dp。再设一个 sum[u][i] 表示 u 为跟,高度在 i 以内的满二叉树个数之和。转移的话就不断的往上跳father,我们设新增的方案数为 new,当前点的儿子的dp值为 last(特殊地,最开始的 last=0),则:
new=new\times(sum[u][dept_i]-last)$,$dp[u][dept_i]+=new,sum[u][dept_i]+=更新前的new,注意要取模。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3*114514,M=1919810,mod=998244353;
ll n,p[N];
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll dp[N][32],sum[N][32],dept[N],lim;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n; lim=20;
	dept[1]=1,dp[1][1]=1,p[1]=0;
	cout<<"1\n";
	for(int i=2;i<=n;++i){
		cin>>p[i]; ll ans=0;
		dept[i]=dept[p[i]]+1;
		if(dept[i]<=lim){ //我懂了,直接continue连答案都没有输出(
			dp[i][dept[i]]=1; //初始是有一的
			ll last=0,newv=1,tmp=0;
			for(int u=p[i];u;u=p[u]){
				tmp=newv;
				newv=newv*(sum[u][dept[i]]-last+mod)%mod;
				last=dp[u][dept[i]]%mod;
				dp[u][dept[i]]+=newv%mod,dp[u][dept[i]]%=mod;
				sum[u][dept[i]]+=tmp%mod,sum[u][dept[i]]%=mod;
			}
		}
		for(int j=1;j<=lim+5;++j) ans+=dp[1][j]%mod,ans%=mod; 
		cout<<ans<<'\n';
	}
	return 0;
}

CF1572C Paint

看到有连续段的条件并且允许 O(n2),考虑优化区间dp。我们首先的朴素想法是设 dp[i][j][k][i,j] 区间 染成 k 颜色的最小操作次数,复杂度 O(n4)。我们尝试发掘一些性质,手玩之后珂以发现:整个区间染成 ai 颜色一定是最优的,所以我们便不需要去枚举 k 了。此时还是朴素的区间dp,复杂度 O(n3)
继续考虑优化,我们注意到题目中的特殊限制,猜想能不能将枚举断点 k 这一步转化成枚举一种颜色的所有位置。我们发现在朴素的染色方式,也就是一个一个染最后染 n 次的方案的基础上,每个形如 aba 的区间都可以将染色次数减一,那么我们就考虑重设 dp[i][j][i,j] 区间能优化多少次。这样我们就不必去枚举区间中的所有断点,而是只去枚举和 i 颜色相同的点进行转移,dp[i][j]=max(dp[i][j],dp[i+1][k1]+dp[k][j]+1),这样子复杂度就降到了 O(20n2),就能通过本题。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3005,M=1919810;
ll T;
ll n,a[N],dp[N][N];
void solve(){
	cin>>n;
	vector <ll> g[N];
	for(int i=1;i<=n;++i){
		cin>>a[i],g[a[i]].push_back(i);
		for(int j=1;j<=n;++j) dp[i][j]=0;
	}
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			dp[i][j]=dp[i+1][j]; //先继承子区间的答案 
			for(int k:g[a[i]])
				if(i<k&&k<=j) dp[i][j]=max(dp[i][j],dp[i+1][k-1]+1+dp[k][j]);
		}
	cout<<n-dp[1][n]-1<<'\n'; //答案就是n-优化次数-1 
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>T;
	while(T--) solve();
	return 0;
} 

CF1860D

看到这个数据范围珂以搞区间dp,那我们设 dp[i][j][i,j] 区间的最少交换次数,长度小于等于二的区间都设为 inf。然后我们考虑怎么得知两个数交换后顺逆序对个数的变化情况,很明显交换只会影响两个点中间那段区间的顺逆序对数,那么我们可以考虑对与每个点维护一个顺逆序对数的前缀和,这样子就珂以 O(1) 得出了。
其实这道题我们珂以考虑贡献法,只考虑 1 的贡献,每个 1 的贡献就是前面 0 的个数减去后面 0 的个数,当数列中的 1 的贡献之和为 0 时这个数列符合题意。我们珂以对着思路设状态,设 dp[i][j][k] 为dp到第 i 个数,有
j1,总贡献为 k 时的与原数列不同的位置数。转移:dpi,j,k=min(dpi1,j,k,dpi1,j1,k+2(ij)cnt0+(ai=0))。这里待补。
注意到贡献可能为负数,那我们就给 k 加上 nn/2 进行转移,这样子时空复杂度都为 O(n4),时间还好空间就爆了,很简单地发现 i 那一位珂以滚掉,那么就不会爆了。最后的答案便是 dp[ncnt0][nn/2]/2,毕竟我们设的状态是有多少个不同的数,最后答案要除以二。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114,M=1919810;
ll n,m,a[N],cnt; string s;
ll dp[N][N*N]; //炸了,滚了 
int main(){
	cin>>s; n=s.size(),m=n*n/2; //贡献正负分界 
	for(int i=1;i<=n;++i) a[i]=s[i-1]-'0',cnt+=!a[i];
	memset(dp,63,sizeof(dp));
	//for(int i=0;i<=n;++i) dp[i][0][m]=0;
	dp[0][m]=0;
	for(int i=1;i<=n;++i)
		for(int j=i;j>=0;--j)
			for(int k=-m;k<=m;++k){
				dp[j][k+m]=dp[j][k+m]+a[i];
				if(j) dp[j][k+m]=min(dp[j][k+m],dp[j-1][k+m+2*(i-j)-cnt]+(!a[i]));
			}
	cout<<dp[n-cnt][m]/2;
	return 0;
}

CF1000G Two-Paths

本来还以为挺简单的,结果越想越不对,然后从昨天晚上写到今天早上。稍微观察一下就珂以发现这是个换根,考虑维护一些值:fu 表示以 1 为根时 u 子树内可得的最大贡献,gu 表示以 1 为根时 u 的父亲的子树内不走 u 的子树时的最大贡献,dpu 表示以 u 为起点可获得的最大贡献(珂以不走子树),这些珂以换根预处理出来,并且我们都默认每条边走两遍,注意 fu,gu 要从叶子向上处理。
然后查询的时候考虑分情况讨论:当 u,v 在同一条链上时我们假设 u 是深度较大的点,贡献由 u 的子树、除 v 子树的部分和路径上的贡献组成。令 uvu 的方向走一步到达的那个点,我们要减去 gudpv 中的贡献,然后加上 uu 路径上的点的 g 值,加上路径上点的权值(没算到)减去这条路径的边权和(只走一遍不乘二),还要加上 fu 的贡献;
u,v 不在一条链上时我们向上面一样令相同定义的 u,v,两条链的贡献分别计算,并且注意 lca 的贡献是否被重复算过;如果 u=v 贡献就是 dpu。至于找 u,v 珂以通过先倍增找到链的长度(不算边权)然后将长度减一去找到那个点,然后这个题就做完了,细节贼多。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3*114514,M=1919810;
struct xx{
	ll next,to,val;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
}
ll ff[N][32],dept[N],lg[N];
void dfs_pre(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		ff[v][0]=u; dept[v]=dept[u]+1;
		for(int j=1;j<=lg[dept[v]];++j)
			ff[v][j]=ff[ff[v][j-1]][j-1];
		dfs_pre(v,u);
	}
}
ll query_lca(ll a,ll b){
	if(a==b) return a;
	if(dept[a]<dept[b]) swap(a,b);
	for(int i=lg[dept[a]];i>=0;--i)
		if(dept[ff[a][i]]>=dept[b])
			a=ff[a][i];
	if(a==b) return a;
	for(int i=lg[dept[a]];i>=0;--i)
		if(ff[a][i]!=ff[b][i]){
			a=ff[a][i];
			b=ff[b][i];
		}
	return ff[a][0];
}
ll n,q,a[N],dis[N],val[N],siz[N]; //val记录边权,siz是点权和,dis记录路径权值 
ll f[N],g[N],dp[N];
//1为根时子树内的最大贡献/当前点的父亲走自己子树内的点且不走当前点子树时的最大贡献
//以i为起点能得到的最大贡献
void dfs1(ll u,ll fa){
	siz[u]=f[u]=a[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		dis[v]=dis[u]+w; val[v]=w;
		dfs1(v,u); //应该从叶子开始 
	}
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		f[u]+=max(f[v]-2*w,0ll);
	}
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		g[v]=f[u]-a[u]-max(f[v]-2*w,0ll);
	}
}
void dfs2(ll u,ll fa,ll ew){
	if(u!=1) dp[u]=max(dp[fa]-max(f[u]-2*ew,0ll)-2*ew,0ll)+f[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		g[v]+=g[u];
		siz[v]+=siz[u];
		dfs2(v,u,w);
	}
}
ll find_dis(ll u,ll v){
	ll dis=0;
	for(int i=lg[dept[u]];i>=0;--i)
		if(dept[ff[u][i]]>=dept[v]) u=ff[u][i],dis+=(1<<i);
	return dis; 
}
ll find_pos(ll u,ll k){ //倍增找点 
	for(int i=lg[dept[u]];i>=0;--i)
		if((k>>i)&1) u=ff[u][i];
	return u;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	for(int i=1;i<n;++i){
		ll u,v,w;
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);
	}
	dept[1]=1,dfs_pre(1,0);
	dfs1(1,0); dp[1]=f[1];
	dfs2(1,0,0);
	for(int i=1;i<=q;++i){
		ll u,v,lca,d,ans=0;
		cin>>u>>v; lca=query_lca(u,v);
		d=dis[u]+dis[v]-2*dis[lca];
		if(u==v) cout<<dp[u]<<'\n';
		else if(lca==u||lca==v){
			if(u==lca) swap(u,v); //默认u深度更大
			ll diss=find_dis(u,v),x=find_pos(u,diss-1);
			ans+=f[u]-a[u]+g[u]-g[x]+dp[v];
			ans-=max(f[x]-2*val[x],0ll);
			ans-=d; ans-=a[v];
			ans+=siz[u]-siz[ff[v][0]];
			cout<<ans<<'\n';
		}
		else{
			ll u2=find_pos(u,find_dis(u,lca)-1),v2=find_pos(v,find_dis(v,lca)-1);
			ans+=f[u]+f[v]-a[u]-a[v]-a[lca];
			ans+=g[u]+g[v]-g[u2]-g[v2];
			ans+=siz[u]+siz[v]-siz[lca]-siz[ff[lca][0]];
			ll x=max(f[u2]-2*val[u2],0ll),y=max(f[v2]-2*val[v2],0ll);
			ans+=dp[lca]-d-x-y;
			cout<<ans<<'\n';
		}
	}
	return 0;
}

AT dp_t

一眼没思路,但是较简单。考虑设 dpi,j 为dp到第 i 个位置并且填 j 时的方案数。往后转移时为了保证数列是排列我们可以将 j 的数都加一。我们枚举第 i1 个数(设为 k)那么转移便是 dpi,j={k=1j1dpi1,k,si1=<k=ji1dpi1,k,si1=>,时间复杂度 O(n3)。考虑优化掉枚举 k 的那一步,我们发现其实这个式子中含 k 的那部分其实就是个前缀和,那就直接空间换时间就行了。这种题看着挺眼熟的。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3001,M=1919810,mod=1e9+7;
ll n; bool f[N];
ll dp[N][N]; //dp到第i位,第i位填j
ll sum[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i=1;i<n;++i){
		char ch; cin>>ch;
		f[i]=(ch=='<');
	}
	for(int i=1;i<=n;++i) dp[1][i]=sum[1][i]=1;
	for(int i=2;i<=n;++i)
		for(int j=1;j<=i;++j){
			if(f[i-1]) dp[i][j]+=sum[i-1][j-1],dp[i][j]%=mod;
			else dp[i][j]+=(sum[i-1][i-1]-sum[i-1][j-1]+mod)%mod,dp[i][j]%=mod;
			sum[i][j]=sum[i][j-1]+dp[i][j],sum[i][j]%=mod;
		}
	ll ans=0;
	for(int i=1;i<=n;++i) ans+=dp[n][i],ans%=mod;
	cout<<ans%mod;
	return 0;
} 

ABC142E

状压,思维难度没啥,主要是当作一个借鉴,我还不太会写状压。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,mod=1e9+7;
ll n,m,dp[N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<(1<<n);++i) dp[i]=mod;
	//初始化不能初始0 
	for(int j=1;j<=m;++j){
		ll a,b,c,sum=0;
		cin>>a>>b;
		while(b--){
			cin>>c;
			sum|=(1<<(c-1));
		}
		for(int i=0;i<(1<<n);++i) dp[i|sum]=min(dp[i|sum],dp[i]+a); 
	}
	if(dp[(1<<n)-1]==mod) cout<<-1;
	else cout<<dp[(1<<n)-1];
	return 0;
}

ABC130E

变化了一下的 LCS,虽然求的不是 LCS 但过程类似。我们还是设 a 的前 i 个和 b 的前 j 个数中的公共子序列的数量,首先 dp[i][j] 是珂以直接从 dp[i1][j],dp[i][j1] 转移过来的,但是多加了一个 dp[i1][j1] 要减掉,若 a[i]=b[j]dp[i][j]+=dp[i1][j1] 相当于是 dp[i1][j1] 的所有方案都多了 1,记得初始化边界为 1

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*1145,M=1919810,mod=1e9+7;
ll n,m,a[N],b[N];
ll dp[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=m;++i) cin>>b[i];
	for(int i=0;i<=n;++i) dp[i][0]=1;
	for(int j=0;j<=m;++j) dp[0][j]=1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j){
			dp[i][j]=(dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mod)%mod;
			if(a[i]==b[j]) dp[i][j]+=dp[i-1][j-1],dp[i][j]%=mod;
		}
	cout<<dp[n][m];
	return 0;
}

P4099 [HEOI2013] SAO

淦忘记保存了。这个题多头写一个“求一个树的拓扑序数量”我还以为是啥水题,结果难得多。借鉴了一下。

考虑将有向图直接看成一颗树进行树形dp,设状态 dp[u][i] 表示在 u 的子树中 u 拓扑序上排名为 i 时的方案数。

考虑如何从儿子 v 转移到父亲 u,我们珂以理解为是不断合并合并 u,v 子树的拓扑序。先初设 dp[u][1]=1,先考虑 u 的排名在 v 前的限制情况,我们设合并前 u 的拓扑序排名为 i,合并后有 j 个数排在 u 前面,v 的排名为 k,感性理解一下怎么转移:我们现在要合并两个拓扑序序列,第一个序列中 u 在第 i 位,在第二个序列中 u 在第 k 位,要得出合并的方案数。将第二个序列也就是 v 的子树合并起来本身就是有组合方案的,即 v 子树插到 u 中的方案,组合数一部分是 C(i+j1j),其组合意义是在拓扑序位于 u 前的 i+j1 个元素中选 j 个元素出来;另一部分则是拓扑序在 v 后的 siz[u]+siz[v]ij 个元素中放入剩下 siz[v]j 个元素的方案,这样两者的积便是 C(i+j1j)×C(siz[u]+siz[v]ijsiz[v]j)。考虑转移范围,显然 1isizu,1ksizv,因为做多有 k1v 子树中元素在 u 前,所以 0j<k

于是我们令两者之积为一个常数 K,珂以得到转移式:dp[u][i+j]+=dp[u][j]×dp[v][k]×K,但是这个 dp[u][i] 是会被改变的,转移有后效性,所以我们珂以另开一个 tmp 记录下原来的 dp[u][i],并且要把 dp[u][i] 清零,然后代入转移。

u 排在 v 后面的情况也差不多,组合的方案是一样的,但是 v 已经在 u 前面了,所以 j 的范围是 kjsizv

然后考虑复杂度,要枚举 i,j,k 总共是 n3 的,过不了。考虑能不能将某一维转化成另一维,先将枚举转化为求和式,可以这样得到:
于是枚举 k 就被优化掉了,我们珂以把 dp[v][k] 提出来变为前缀和的形式,就将复杂度降至 O(n2),可通过。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2005,M=1919810,mod=1e9+7;
struct xx{
	ll next,to,fl;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].fl=z;
	head[x]=cnt;
}
ll T,c[N][N];
ll n,a[N],dp[N][N]; //u子树内排j时方案数 
ll siz[N],tmp[N];
void dfs(ll u,ll fa){
	siz[u]=1,dp[u][1]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,fl=e[i].fl;
		if(v==fa) continue;
		dfs(v,u);
		for(int j=1;j<=siz[u];++j) tmp[j]=dp[u][j],dp[u][j]=0;
		if(!fl){
			for(int j=1;j<=siz[u];++j)
				for(int k=0;k<=siz[v];++k)
					dp[u][j+k]=(dp[u][j+k]+tmp[j]*(dp[v][siz[v]]-dp[v][k]+mod)%mod*
					c[j+k-1][k]%mod*c[siz[u]+siz[v]-j-k][siz[v]-k]%mod)%mod;
		}
		else{
			for(int j=1;j<=siz[u];++j)
				for(int k=1;k<=siz[v];++k) 
					dp[u][j+k]=(dp[u][j+k]+tmp[j]*dp[v][k]%mod*
					c[j+k-1][k]%mod*c[siz[u]+siz[v]-j-k][siz[v]-k]%mod)%mod;
		}
		siz[u]+=siz[v];
	}
	for(int i=1;i<=siz[u];++i) dp[u][i]=(dp[u][i]+dp[u][i-1])%mod;
}
void solve(){
	memset(dp,0,sizeof(dp));
	memset(head,0,sizeof(head));
	cnt=0;
	cin>>n;
	for(int i=1;i<n;++i){
		ll a,b; char ch;
		cin>>a>>ch>>b; ++a,++b;
		if(ch=='<') add(a,b,1),add(b,a,0);
		else add(a,b,0),add(b,a,1);
	}
	dfs(1,0);
	cout<<dp[1][n]<<'\n';
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	for(int i=0;i<=2000;++i) c[i][0]=c[i][i]=1;
	for(int i=1;i<=2000;++i)
		for(int j=1;j<=i;++j)
			c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
	cin>>T;
	while(T--) solve();
	return 0;
}
posted @   和蜀玩  阅读(65)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示