设计类 DP 学习笔记

主要涉及到状态和转移的设计与优化的一类 DP。(大概)普遍具有较低的思维难度但是较高的实现难度。

I.状态优化类 DP

通过改变状态本身的定义以降低 DP 复杂度。

I.[JSOI2010]快递服务

我们约定共有n个地点,依次登记了m家公司。

思路1.

f[l][i][j][k]表示:当前某一个司机在第i家公司(注意是公司!1000家那个!),第二个司机在第j家,第三个司机在第k家,当前我们遍历到了第l家公司。依次转移即可。

复杂度O(m4)

思路2.观察到i,j,k中必有一个等于l(不然你位置l的货是哪辆车发的?),因此我们可以省掉一维。设f[i][j][k]即可。

复杂度O(m3)

明显第一维可以滚动掉,因此空间复杂度便可以通过。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,dis[210][210],f[2][1010][1010],pos[1010],m,res=0x3f3f3f3f;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)scanf("%d",&dis[i][j]);
	memset(f,0x3f3f3f3f,sizeof(f)),f[1][2][1]=0;
	pos[++m]=1,pos[++m]=2,pos[++m]=3;
	while(scanf("%d",&pos[++m])!=EOF);
	for(int i=3;i<m;i++){
		for(int j=1;j<=i;j++)for(int k=1;k<j;k++)f[!(i&1)][j][k]=0x3f3f3f3f;
		for(int j=1;j<i;j++)for(int k=1;k<j;k++){
			f[!(i&1)][j][k]=min(f[!(i&1)][j][k],f[i&1][j][k]+dis[pos[i]][pos[i+1]]);
			f[!(i&1)][i][k]=min(f[!(i&1)][i][k],f[i&1][j][k]+dis[pos[j]][pos[i+1]]);
			f[!(i&1)][i][j]=min(f[!(i&1)][i][j],f[i&1][j][k]+dis[pos[k]][pos[i+1]]);
		}
//		for(int j=1;j<=i;j++){for(int k=1;k<j;k++)printf("%d ",f[!(i&1)][j][k]);puts("");}puts("");
	}
	for(int j=1;j<m;j++)for(int k=1;k<j;k++)res=min(res,f[m&1][j][k]);
	printf("%d\n",res);
	return 0;
} 

思路3.发现O(m3)只能拿到50%。似乎只有最后两维可以优化成n2的。

我们设f[i][j][k]表示:当前某一辆车在第i公司(还是1000家那个!)剩下两辆车分别在第j和第k收件地点(是200家那个!)。

复杂度为O(mn2)

这是正解尽管出题人丧心病狂卡长只有70%但是开个O3然后卡卡长就过了

代码:

#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
int n,dis[210][210],f[2][1010][1010],pos[1010],m,res=0x3f3f3f3f;
inline void read(int &x){
	x=0;
	register char c=getchar();
	while(c>'9'||c<'0')c=getchar();
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
inline void print(int x){
	if(x<=9)putchar('0'+x);
	else print(x/10),putchar('0'+x%10);
}
int main(){
	read(n);
	for(register int i=1;i<=n;i++)for(register int j=1;j<=n;j++)read(dis[i][j]);
	memset(f,0x3f3f3f3f,sizeof(f)),f[1][2][1]=0;
	pos[++m]=1,pos[++m]=2,pos[++m]=3;
	while(scanf("%d",&pos[++m])!=EOF);
	for(register int i=3;i<m;i++){
		for(register int j=1;j<=n;j++)for(register int k=1;k<=n;k++)f[!(i&1)][j][k]=0x3f3f3f3f;
		for(register int j=1;j<=n;j++)for(register int k=1;k<=n;k++){
			f[!(i&1)][j][k]=min(f[!(i&1)][j][k],f[i&1][j][k]+dis[pos[i]][pos[i+1]]);
			f[!(i&1)][pos[i]][k]=min(f[!(i&1)][pos[i]][k],f[i&1][j][k]+dis[j][pos[i+1]]);
			f[!(i&1)][pos[i]][j]=min(f[!(i&1)][pos[i]][j],f[i&1][j][k]+dis[k][pos[i+1]]);
		}
//		for(int j=1;j<=i;j++){for(int k=1;k<j;k++)printf("%d ",f[!(i&1)][j][k]);puts("");}puts("");
	}
	for(register int j=1;j<=n;j++)for(register int k=1;k<=n;k++)res=min(res,f[m&1][j][k]);
	print(res);
	return 0;
} 

II.[SCOI2009]粉刷匠

所有的DP,只要式子一推出来(不管复杂度),那就很简单了,因为优化是成千上万种的……

思路1.我们考虑设f[i][j][k]表示:当前DP到第i块木板的第j个位置,共涂了k次,所能获得的最大收益。因为还要枚举当前这次涂是从哪到哪的,因此复杂度为O(NM2T),实际90%。在实际操作中,第一维可以省略。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,t,f[100][5000],res[100][100][2],ans;
char s[100];
int main(){
	scanf("%d%d%d",&n,&m,&t);
	for(int l=1;l<=n;l++){
		scanf("%s",s+1);
		for(int i=1;i<=m;i++)for(int j=i;j<=m;j++)res[i][j][0]=res[i][j-1][0]+(s[j]=='0'),res[i][j][1]=res[i][j-1][1]+(s[j]=='1');
		for(int i=0;i<=t;i++)f[0][i]=f[m][i];
		for(int i=1;i<=m;i++){
			for(int j=1;j<=t;j++){
				f[i][j]=0;
				for(int k=0;k<i;k++)f[i][j]=max(f[i][j],f[k][j-1]+max(res[k+1][i][0],res[k+1][i][1]));
			}
		}
	}
	for(int i=1;i<=t;i++)ans=max(ans,f[m][i]);
	printf("%d\n",ans);
	return 0;
}

虽然开个O2甚至只是单纯卡卡长也一样能过,但是介于十年前评测姬的蜜汁速度,我们思考还有没有优化复杂度的余地。

思路2.我们考虑设f[i][j][k][0/1/2]表示:

当前DP到第i块木板的第j个位置,共涂了k次,当前这个位置的状态是0/1/2

其中,状态0意为涂上了颜色0,状态1意为涂上了颜色1,状态2意为啥也没涂。

因为这种方法不需要枚举上一次的断点,因此复杂度是O(NMT)的。老样子,第一维可以砍掉。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,t,f[100][5000][3],res;
char s[100];
int main(){
	scanf("%d%d%d",&n,&m,&t);
	for(int l=1;l<=n;l++){
		scanf("%s",s+1);
		for(int i=0;i<=t;i++)f[0][i][2]=max(max(f[m][i][0],f[m][i][1]),f[m][i][2]);
		for(int i=1;i<=m;i++){
			for(int j=1;j<=t;j++){
				f[i][j][2]=max(max(f[i-1][j][0],f[i-1][j][1]),f[i-1][j][2]);
				f[i][j][1]=max(max(f[i-1][j-1][0],f[i-1][j][1]),f[i-1][j-1][2])+(s[i]=='0');
				f[i][j][0]=max(max(f[i-1][j][0],f[i-1][j-1][1]),f[i-1][j-1][2])+(s[i]=='1');
			}
		}
	}
	for(int i=1;i<=t;i++)res=max(max(res,f[m][i][2]),max(f[m][i][0],f[m][i][1]));
	printf("%d\n",res);
	return 0;
}

III.[HNOI2009]双递增序列

某科学的消减维数

思路1.暴力五维DP:

f[h][i][j][k][l]表示:前h位中,Ui位,Vj位,Uk结尾,Vl结尾是否合法。

显然过不去。

思路2.暴力四维DP:

发现必有i+j=h,因此我们可以消掉ij

则有设f[i][j][k][l]表示:前i位中,Uj位,Uk结尾,Vl结尾是否合法。

思路3.暴力三维DP:

发现UV中必有一个以位置i为结尾。那么我们可以令「序列1」表示以i为结尾的那个串,「序列2」表现另一个串。

f[i][j][k]表示:前i位中,「序列1」有j位,「序列2」以k结尾是否合法。

思路4.正解二维DP:

f数组是一个bool数组时,便有优化的空间。

明显这个k越小越好。因此我们可以设f[i][j]表示:前i位中,「序列1」有j位,此时「序列2」的结尾最小为f[i][j]

代码:

#include<bits/stdc++.h>
using namespace std;
int n,T,f[2010][2010],num[2010];
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n),num[0]=-1;
		for(int i=1;i<=n;i++)scanf("%d",&num[i]);
		for(int i=1;i<=n;i++)for(int j=1;j<=min(i,n>>1);j++)f[i][j]=0x3f3f3f3f;
//		for(int i=1;i<=n;i++){for(int j=1;j<=min(i,n>>1);j++)printf("%d ",f[i][j]);puts("");}
		for(int i=1;i<=n/2;i++){
			if(num[i]>num[i-1])f[i][i]=-1;
			else break;
		}
		for(int i=1;i<n;i++)for(int j=1;j<=min(i,n/2);j++){
			if(num[i+1]>num[i])f[i+1][j+1]=min(f[i+1][j+1],f[i][j]);
			if(num[i+1]>f[i][j])f[i+1][i+1-j]=min(f[i+1][i+1-j],num[i]);
		}
//		for(int i=1;i<=n;i++){for(int j=1;j<=min(i,n>>1);j++)printf("%d ",f[i][j]);puts("");}
		puts(f[n][n/2]!=0x3f3f3f3f?"Yes!":"No!");
	}
	return 0;
}

IV.CF837D Round Subset

思路:

f[l][i][j][k]表示:

l位,选出j个,这j个物品能否拥有j5k2bool型)

接下来开始削减位数。

第一维可以直接01背包掉。现在只剩f[i][j][k]三维。

因为这是bool,我们就可以想办法把它压成int

于是设f[i][j]表示:选择i个物品,拥有j5时,最多能拥有多少个2

则答案为max{min(i,f[m][i])}

复杂度为O(n2mlog5a),可以通过。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[210][13000],lim,res;
pair<int,int>p[210];
pair<int,int>read(){
	long long x;
	scanf("%I64d",&x);
	pair<int,int>ret=make_pair(0,0);
	while(!(x%5))x/=5,ret.first++;
	while(!(x&1))x>>=1,ret.second++;
	return ret;
}
int main(){
	scanf("%d%d",&n,&m),memset(f,-1,sizeof(f));
	for(int i=1;i<=n;i++)p[i]=read(),lim+=p[i].first;
	f[0][0]=0;
	for(int i=1;i<=n;i++)for(int j=min(i,m);j;j--)for(int k=lim;k>=p[i].first;k--)if(f[j-1][k-p[i].first]!=-1)f[j][k]=max(f[j][k],f[j-1][k-p[i].first]+p[i].second);
	for(int i=1;i<=lim;i++)res=max(res,min(i,f[m][i]));
	printf("%d\n",res);
	return 0;
}

V.[TopCoder 12519]ScotlandYard

我们考虑一个最原始的DP状态:f[S]表示根据当前给出的信息,猜的人可以推测出当前藏的人一定在且仅在集合S之中时,藏的人最多可以走多少步。

然后考虑枚举藏的人下一步给出走了什么颜色的边,然后取S中所有点当前颜色的出边集合的并集T,则有f(S)f(T)+1。边界状态为 f()=0f(S)=1 when |S|=1

观察可以发现,猜的人最终可以确定藏的人在哪里的前一刻,S的大小:

  1. 如果3,显然只判断其中两个亦可;

  2. 如果=2,显然可以根据此两个一路反推回去,即任意时刻|S|都可以被缩减到2

  3. 如果1,此状态显然不合法,不可能出现。

故我们发现任意时刻状态中仅需存储S中两个数即可。这样状态数就缩小到O(n2)级别了。则此时就可以直接按照上文所述DP了。采取记忆化搜索的方式DP,如果任意时刻发现搜索到成环了,则答案必为

代码(TC的格式):

#include<bits/stdc++.h>
using namespace std;
class ScotlandYard{
private:
	const int inf=0x3f3f3f3f;
	int n,f[60][60];
	vector<int>g[60][3];
	bool in[60][60];
	int dfs(int x,int y){
		if(in[x][y])return inf;
		if(f[x][y]!=-1)return f[x][y];
		in[x][y]=true;
		f[x][y]=0;
		for(int i=0;i<3;i++){
			vector<int>v;
			for(auto j:g[x][i])v.push_back(j);
			for(auto j:g[y][i])v.push_back(j);
			sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
			if(v.size()==0){f[x][y]=max(f[x][y],0);continue;}
			if(v.size()==1){f[x][y]=max(f[x][y],1);continue;}
			for(int i=0;i<v.size();i++)for(int j=i+1;j<v.size();j++)f[x][y]=max(f[x][y],min(inf,dfs(v[i],v[j])+1));
		}
		in[x][y]=false;
		return f[x][y];
	}
public:
	int maxMoves(vector<string>taxi,vector<string>bus,vector<string>metro){
		n=taxi.size();
		for(int i=0;i<n;i++)for(int j=0;j<3;j++)g[i][j].clear();
		for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(taxi[i][j]=='Y')g[i][0].push_back(j);
		for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(bus[i][j]=='Y')g[i][1].push_back(j);
		for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(metro[i][j]=='Y')g[i][2].push_back(j);
//		for(int i=0;i<3;i++){for(int j=0;j<n;j++){for(auto k:g[j][i])printf("%d ",k);puts("");}puts("");}
		memset(f,-1,sizeof(f));
		int res=0;
		for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)res=max(res,dfs(i,j));
		return res>=inf?-1:res;
	}
}my;

VI.[CEOI2007]树的匹配Treasury

题解

VII.[COCI2019]Mobitel

如果正着来DP的话,状态是 O(rsn) 的,不可能通过。

这时,我们就要应用一些数论知识了:

ai<n

n1ai1

然后,整除又有如下性质:

n1i=1kai=n1i=1k1aiak

同时,依据整除分块的性质,ni的值域大小是 O(n)

因此我们可以保存上述下取整的结果作为DP状态。此时复杂度就来到了 O(rsn)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,lim,p,a[310][310],f[2][310][2010],deco[2010],code[1001000];
int main(){
	scanf("%d%d%d",&n,&m,&lim),lim--;
	deco[1]=lim,code[lim]=1,p=1;
	for(int i=2;i<=lim;i++)if(lim/i!=deco[p])p++,deco[p]=lim/i,code[lim/i]=p;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);
//	for(int i=1;i<=p;i++)printf("%d ",deco[i]);puts("");
	f[n&1][m][code[lim/a[n][m]]]=1;
	for(int i=n;i;i--){
		memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
		for(int j=m;j;j--)for(int k=0;k<=p;k++){
			if(i-1)(f[!(i&1)][j][code[deco[k]/a[i-1][j]]]+=f[i&1][j][k])%=mod;
			if(j-1)(f[i&1][j-1][code[deco[k]/a[i][j-1]]]+=f[i&1][j][k])%=mod;
		}	
	}
	printf("%d\n",f[1][1][0]);
	return 0;
}

VIII.巡游

题意:

你在数轴的原点。在位置 1n 各有着一个物品,位置 i 的物品重量为 ai。你每次可以:向左/右移动一步,消耗你目前拥有物品重量加一的能量;或是取走当前所在位置的物品(假如其尚未被取走)。

求你在拥有 m 点能量时,从原点出发再回到原点,所能够拿取的物品重量之和的最大值。

数据范围:1n106,0ai106,1m3×107

一个显然的观察是,你必然是先一路往右走到某个位置,然后一路往左,在返程中拿去某些 ai。这样,每个 ai 的代价是 iai,并且最大的 i 还会产生 2i 的额外代价。

这明显是背包问题。

但是本题有一个好性质,就是第 i 个物品的单位代价为 i。这意味着我们可以倒序背包,背包到位置 i 时总收益不超过 mi。那么这样就可以减少枚举范围,让复杂度减少到 i=1nmi=mlogn

这复杂度能过吗?

因为背包常数太太太小了,所以能过。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[1001000],f[30001000],res;
void chmn(int&x,ll y){if(x>y)x=y;}
int main(){
	freopen("walk.in","r",stdin);
	freopen("walk.out","w",stdout);
	scanf("%d%d",&n,&m),memset(f,0x3f,sizeof(f)),f[0]=0;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=n;i;i--){
		for(int j=m/i;j>a[i];j--)chmn(f[j],f[j-a[i]]+1ll*i*a[i]);
		chmn(f[a[i]],1ll*i*(a[i]+2));
	}
	for(int i=0;i<=m;i++)if(f[i]<=m)res=i;
	printf("%d\n",res);
	return 0;
}

IX.学校

题意:给定长为 n元素互不相同且均小于 2m 的非负整数序列 a,求满足任意连续四个元素的异或和均不等于给定常数 s 的子序列个数,对 998244353 取模。

数据范围:1n4000,0ai,s<2m,0m12

首先一个显然的 DP 是设 fi,j,k 表示当前子序列末三个元素依次是 i,j,k 时的方案数。枚举上一个元素 p,得到

fi,j,k=1+p<k,aiajakapsfj,k,p

直接按照上式转移,复杂度 O(n4)

现在考虑优化。用一个数组维护 p<kfj,k,p,然后从中减去 p<k,aiajakap=sfj,k,p。后者可以开桶维护。

这样就做到了 n3

进一步优化需要利用 ai 互不相同的性质。这表明任意连续五个元素中,前四个与后四个异或和不可能同为 s

于是我们压缩状态,只记录 fi,j。转移时枚举前一个 k。如果 i,j,k,p 四个元素异或和为 s,则 j,k,p 以及更前一个元素异或和不可能为 s,这样就不会被重复计算。

故现在有式子

fi,j=1+k<j(fj,kp<k,aiajakap=sfk,p)

拆开得

fi,j=1+(k<jfj,k)p<k<j,aiajakap=sfk,p

两部分分开开桶维护即可。复杂度 O(n2+n2m)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int n,m,s,a[4010],f[4010][4010],g[4010],h[4010][1<<12],res;
int main(){
	freopen("school.in","r",stdin);
	freopen("school.out","w",stdout); 
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	res=n;
	for(int j=1;j<=n;j++){
		for(int i=j+1;i<=n;i++){
			f[i][j]=1+g[j];
			(f[i][j]+=mod-h[j][a[i]^a[j]^s])%=mod;
			(res+=f[i][j])%=mod;
			(g[i]+=f[i][j])%=mod;
			
			(h[i+1][a[i]^a[j]]+=f[i][j])%=mod;
		}
		for(int i=0;i<(1<<m);i++)(h[j+1][i]+=h[j][i])%=mod;
	}
	printf("%d\n",res);
	return 0;
}

X.CF1784E Infinite Game

考虑走完一遍 s 后,我们可能剩下半截游戏没结束:这剩下的半截游戏可能是 (0,0),(0,1),(1,0),(1,1) 之一。

于是我们可以记录在 s 开头时,尚未结束的半截游戏是 (0,0),(0,1),(1,0),(1,1) 的场合下,走到每个位置时:

  • 现在的场合。
  • 现在双方胜场差。

胜场差是 O(n) 的量。还要记一个位置,复杂度 O(n5),不太行。

注意到这个东西的问题,是在于要对 (0,0),(0,1),(1,0),(1,1) 四个初态各记一个当前态,因此复杂度较高。

我们不如首先枚举前两个位置的值,这样可以把四个初态全都推进到 (0,0) 的阶段态,此时它们可能位于 0,1,2 三处。于是我们只需记录自 0,1,2 三个位置开始时,走到某位时的场合及胜场差,复杂度 O(n4)

但是我们真的需要三个态都记录吗?我们的循环可能是 0101,可能是 0202,也可能是 012012021021000000,或是 022222,011111,0121212 这种开头一个零、最终可以被忽略的场合,但是因为每个初态对应的终态均是确定的,所以我们只需选取我们需要的几个的胜场差然后一起统计即可。复杂度 O(n2),带一个挺大的常数。

个人实现的代码非常糟糕。应该有比我更好的写法,但是太懒不管了。

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
char s[210];
int n;
int res[3];
int f[210][4][4][4][1210];
int consider(int&x,char c){
	if(x==0)return x=(c=='a'?1:2),0;
	if(x==1)return c=='a'?(x=0,1):(x=3,0);
	if(x==2)return c=='b'?(x=0,-1):(x=3,0);
	if(x==3)return x=0,c=='a'?1:-1;
}
int main(){
	scanf("%s",s),n=strlen(s);
	if(n==1){
		if(*s=='a')puts("1 0 0");
		if(*s=='b')puts("0 0 1");
		if(*s=='?')puts("1 0 1");
		return 0;
	}
	for(auto _0:{'a','b'})if(s[0]=='?'||s[0]==_0)
	for(auto _1:{'a','b'})if(s[1]=='?'||s[1]==_1){
		// printf("<%c,%c>\n",_0,_1);
		for(int _=0;_<8;_++){
			int x=0,y=0,z=0,o=0;
			_&1?o+=consider(x,_0):consider(x,_0);
			_&1?o+=consider(x,_1):consider(x,_1);
			_&2?o+=consider(y,_1):consider(y,_1);
			// printf("%d:\n",_);
			memset(f,0,sizeof(f));
			f[2][x][y][z][o+n*3]++;
			for(int i=2;i<n;i++)
				for(x=0;x<4;x++)for(y=0;y<4;y++)for(z=0;z<4;z++)
					for(o=-n*3;o<=n*3;o++)if(f[i][x][y][z][o+n*3])
						for(auto _i:{'a','b'})if(s[i]=='?'||s[i]==_i){
							int X=x,Y=y,Z=z,O=o;
							_&1?O+=consider(X,_i):consider(X,_i);
							_&2?O+=consider(Y,_i):consider(Y,_i);
							_&4?O+=consider(Z,_i):consider(Z,_i);
							(f[i+1][X][Y][Z][O+n*3]+=f[i][x][y][z][o+n*3])%=mod;
						}
			for(x=0;x<4;x++)for(y=0;y<4;y++)for(z=0;z<4;z++)
				for(o=-n*3;o<=n*3;o++)if(f[n][x][y][z][o+n*3]){
					int X=x,Y=y,Z=z;
					int ex=0,ey=0,ez=0;
					int px=0,py=0,pz=0;
					if(X)ex+=consider(X,_0),px++;
					if(X)ex+=consider(X,_1),px++;
					if(Y)ey+=consider(Y,_0),py++;
					if(Y)ey+=consider(Y,_1),py++;
					if(Z)ez+=consider(Z,_0),pz++;
					if(Z)ez+=consider(Z,_1),pz++;
					int cc[3]={0,0,0},oo=0,__=0;
					while(true){
						switch(oo){
							case 0:oo=px;break;
							case 1:oo=py;break;
							case 2:oo=pz;break;
						}
						if(cc[oo]==2)break;
						cc[oo]++;
					}
					for(int i=0;i<3;i++)if(cc[i]==2)__|=1<<i;
					if(__!=_)continue;
					// printf("<%d,%d,%d>=%d\n",px,py,pz,_);
					// printf("%d,%d,%d,%d:%d\n",x,y,z,o,f[n][x][y][z][o+n*3]);
					int O=o;
					if(_&1)O+=ex;
					if(_&2)O+=ey;
					if(_&4)O+=ez;
					if(O>0)(res[0]+=f[n][x][y][z][o+3*n])%=mod;
					if(O==0)(res[1]+=f[n][x][y][z][o+3*n])%=mod;
					if(O<0)(res[2]+=f[n][x][y][z][o+3*n])%=mod;
				}
		}
	}
	printf("%d %d %d\n",res[0],res[1],res[2]);
	return 0;
}

II.转移优化类 DP

一类题目设计状态是简单的,但是构造算法来转移却是复杂的。

I.CF264B Good Sequences

状态很显然。设f[i]表示位置i的最长长度。

关键是转移——暴力转移是O(n2)的。我们必须找到一个更优秀的转移。

因为一个数的质因子数量是O(logn)的,而只有和这个数具有相同质因子的数是可以转移的;

因此我们可以对于每个质数p,设一个mxp表示所有有p作为质因子的xfi的最大值。

关于质因子应该怎么得出嘛……线性筛一下即可。

复杂度O(nlogn)

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5;
int n,pri[N+10],pre[N+10],mx[N+10],f[N+10],res;
void ural(){
	for(int i=2;i<=N;i++){
		if(!pri[i])pri[++pri[0]]=i,pre[i]=pri[0];
		for(int j=1;j<=pri[0]&&i*pri[j]<=N;j++){
			pri[i*pri[j]]=true,pre[i*pri[j]]=j;
			if(!(i%pri[j]))break;
		}
	}
}
int main(){
	scanf("%d",&n),ural();
	for(int i=1,x,t;i<=n;i++){
		scanf("%d",&x),f[i]=1;
		t=x;
		while(t!=1)f[i]=max(f[i],mx[pre[t]]+1),t/=pri[pre[t]];
		t=x;
		while(t!=1)mx[pre[t]]=f[i],t/=pri[pre[t]];
		res=max(res,f[i]);
	}
	printf("%d\n",res);
	return 0;
}

II.[USACO20OPEN]Sprinklers 2: Return of the Alfalfa P

首先,一个合法的方案,肯定是有一条从左到右向下延伸的轮廓线:

例如:

其中,蓝色系格子是玉米,红色系格子是苜蓿;浅蓝色位置必须放玉米喷射器,深红色格子必须放苜蓿喷射器。深蓝和浅红格子放不放均可。更一般地说,所有的转角处,都是必须放喷射器的位置。

因此我们可以考虑DP:

假设一定至少放了一个玉米喷射器(有可能有没有任何玉米喷射器的情况,但当且仅当左下角可以放喷射器时,这时只要在左下角放一个苜蓿,其他位置就可以随便放或不放喷射器了),则设f[i][j]表示在位置(i,j)放了一个玉米时的方案数。

我们思考一下,当位置(i,j)已经被放入玉米后,有哪些位置的发射器种类以及决定了:

如图,五角星格子就是(i,j)

那么(i,j)左下角的深蓝色格子肯定已经被决定了;

(i1)行上,肯定有一个深红格子存在(不然(i1)行就没有颜色了),因此实际上,只有以(i,j+1)为左上角的矩形,里面的颜色尚未决定。

因此我们设一个前缀和si,j,表示以(i,j)为左上角的矩形里面有多少个位置没有牛。

先考虑初始化。


  1. 位置(i,j)i1,即不位于第1行。

如图,则位置(i1,1)必有一个苜蓿。显然,只有位置(i1,1)不是牛,该位置才可以作为起始点。

f[i][j]=2s1,1si,j+12


  1. i=1

位置(i1,1)的那个苜蓿不需要放,直接有

f[i][j]=2s1,1si,j+11


考虑转移。

我们枚举一个(k,l),且k<i,l<j

则位置(i1,l+1)肯定有个苜蓿。如果位置(i1,l+1)没有牛,则可以转移。

如图,黄星要想从紫星转移来,那么深红位置是必须放置苜蓿的。

则有f[i][j]=k=1i1l=1j1f[k][l]2sk,l+1si,j+12[(i1,l+1)没有牛]

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,s[2010][2010],f[2020][2020],bin[4001000],res;
char g[2010][2010];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%s",g[i]+1);
	for(int i=n;i;i--)for(int j=n;j;j--)s[i][j]=s[i+1][j]+s[i][j+1]-s[i+1][j+1]+(g[i][j]=='.');
	bin[0]=1;
	for(int i=1;i<=s[1][1];i++)bin[i]=(bin[i-1]<<1)%mod;
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",s[i][j]);puts("");}
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
		if(g[i][j]=='W')continue;
		if(g[i-1][1]=='.')f[i][j]=bin[s[1][1]-s[i][j+1]-2];
		if(i==1)f[i][j]=bin[s[1][1]-s[i][j+1]-1];
		for(int k=1;k<i;k++)for(int l=1;l<j;l++){
			if(g[k][l]=='W'||g[i-1][l+1]=='W')continue;
			(f[i][j]+=1ll*bin[s[k][l+1]-s[i][j+1]-2]*f[k][l]%mod)%=mod;
		}
		if(g[n][j+1]=='.')(res+=1ll*f[i][j]*bin[s[i][j+1]-1]%mod)%=mod;
		if(j==n)(res+=f[i][j])%=mod;
	}
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",f[i][j]);puts("");}
	if(g[n][1]=='.')(res+=bin[s[1][1]-1])%=mod;
	printf("%d\n",res);
	return 0;
}

很明显这种东西是O(n4)的,期望得分24%。考虑优化。


初始化过程是O(n2)的,没问题。关键是转移的地方。

我们搬出式子:

f[i][j]=k=1i1l=1j1f[k][l]2sk,l+1si,j+12[(i1,l+1)没有牛]

先把这个东西拆成和(i,j)有关的和(k,l)有关的部分:

f[i][j]=k=1i1l=1j1f[k][l]2sk,l+1[(i1,l+1)没有牛]2si,j+1+2

再调整求和顺序:

f[i][j]=l=1j1[(i1,l+1)没有牛]k=1i1f[k][l]2sk,l+12si,j+1+2

然后设一个前缀和sum1[i][j]=k=1if[k][j]2sk,j+1

往里面一代:

f[i][j]=l=1j1[(i1,l+1)没有牛]sum1[i1][l]2si,j+1+2

这样复杂度就被优化成了O(n3),期望得分54%

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int inv2=5e8+4;
int n,s[2010][2010],f[2020][2020],bin[4001000],inv[4001000],res,sum[2020][2020];
char g[2010][2010];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%s",g[i]+1);
	for(int i=n;i;i--)for(int j=n;j;j--)s[i][j]=s[i+1][j]+s[i][j+1]-s[i+1][j+1]+(g[i][j]=='.');
	bin[0]=inv[0]=1;
	for(int i=1;i<=s[1][1];i++)bin[i]=(bin[i-1]<<1)%mod,inv[i]=(1ll*inv[i-1]*inv2)%mod;
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",s[i][j]);puts("");}
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
		if(g[i][j]!='W'){
			if(g[i-1][1]=='.')f[i][j]=bin[s[1][1]-s[i][j+1]-2];
			if(i==1)f[i][j]=bin[s[1][1]-s[i][j+1]-1];
			for(int k=1;k<j;k++){
				if(g[i-1][k+1]=='W')continue;
				(f[i][j]+=1ll*sum[i-1][k]*inv[s[i][j+1]+2]%mod)%=mod;
			}
			if(g[n][j+1]=='.')(res+=1ll*f[i][j]*bin[s[i][j+1]-1]%mod)%=mod;
			if(j==n)(res+=f[i][j])%=mod;			
		}
		sum[i][j]=(1ll*f[i][j]*bin[s[i][j+1]]+sum[i-1][j])%mod;
	}
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",f[i][j]);puts("");}
	if(g[n][1]=='.')(res+=bin[s[1][1]-1])%=mod;
	printf("%d\n",res);
	return 0;
}

继续尝试优化。


f[i][j]=l=1j1[(i1,l+1)没有牛]sum1[i1][l]2si,j+1+2

发现我们现在就可以设一个sum2[i][j]=l=1j[(i,l+1)没有牛]sum1[i][l]

则直接有f[i][j]=sum2[i1][j1]2si,j+1+2

复杂度O(n2),期望得分100%

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int inv2=5e8+4;
int n,s[2010][2010],f[2020][2020],bin[4001000],inv[4001000],res,sum1[2020][2020],sum2[2020][2020];
char g[2010][2010];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%s",g[i]+1);
	for(int i=n;i;i--)for(int j=n;j;j--)s[i][j]=s[i+1][j]+s[i][j+1]-s[i+1][j+1]+(g[i][j]=='.');
	bin[0]=inv[0]=1;
	for(int i=1;i<=s[1][1];i++)bin[i]=(bin[i-1]<<1)%mod,inv[i]=(1ll*inv[i-1]*inv2)%mod;
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",s[i][j]);puts("");}
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
		if(g[i][j]!='W'){
			if(g[i-1][1]=='.')f[i][j]=bin[s[1][1]-s[i][j+1]-2];
			if(i==1)f[i][j]=bin[s[1][1]-s[i][j+1]-1];
			(f[i][j]+=1ll*sum2[i-1][j-1]*inv[s[i][j+1]+2]%mod)%=mod;
			if(g[n][j+1]=='.')(res+=1ll*f[i][j]*bin[s[i][j+1]-1]%mod)%=mod;
			if(j==n)(res+=f[i][j])%=mod;			
		}
		sum1[i][j]=(1ll*f[i][j]*bin[s[i][j+1]]+sum1[i-1][j])%mod;
		sum2[i][j]=(sum2[i][j-1]+(g[i][j+1]=='W'?0:sum1[i][j]))%mod;
	}
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",f[i][j]);puts("");}
	if(g[n][1]=='.')(res+=bin[s[1][1]-1])%=mod;
	printf("%d\n",res);
	return 0;
}

III.[USACO09MAR]Cleaning Up G

n2的DP非常easy,考虑如何优化。

首先,答案一定是n的,因为一定可以每一个数单独划一组,此时答案为n

则一组里面最多只能有n个不同的数,不然平方一下就超过n了。

因此我们可以设posi表示不同的数有i个时,最远能够延伸到哪里。

再设f[i]表示位置i的答案。

f[i]=minj=1n(f[posj]+j2)

关键是如何维护pos。我们只需要对于每个位置记录前驱prei,后继sufi即可。如果一个位置是第一次出现,必有prei<posj;这时就应该删去第一个有sufk>ik

复杂度O(nn)

代码:

#include<bits/stdc++.h>
using namespace std;
int a[40100],n,m,f[40100],lim,pre[40100],suf[40100],pos[40100],val[40100],cnt[40100];
int main(){
	scanf("%d%d",&n,&m),lim=sqrt(n),memset(f,0x3f3f3f3f,sizeof(f)),f[0]=0;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),pre[i]=val[a[i]],suf[pre[i]]=i,suf[i]=n+1,val[a[i]]=i;
//	for(int i=1;i<=n;i++)printf("(%d %d)\n",pre[i],suf[i]);
	for(int i=1;i<=lim;i++)pos[i]=1;
	for(int i=1;i<=n;i++)for(int j=1;j<=lim;j++){
		cnt[j]+=(pre[i]<pos[j]);
		if(cnt[j]>j){
			cnt[j]--;
			while(suf[pos[j]]<=i)pos[j]++;
			pos[j]++;
		}
		f[i]=min(f[i],f[pos[j]-1]+j*j);
	}
//	for(int i=1;i<=n;i++)printf("%d ",f[i]);puts("");
	printf("%d\n",f[n]);
	return 0;
}

IV.[USACO12OPEN]Bookshelf G

转移很简单,直接设f[i]表示前i个位置书架的最小高度和即可。

考虑转移。

我们有暴力的公式

f[i]=minj=1i{fj1+max{hj,,hi}}

因为当i不变时,随着j的增长,那个max是单调不升的。因此我们可以用单调队列维护max的转折点,因为max一定是由一段一段组成的。

因为一整段里面的max都是一样的,因此它就只取决于那个fj。因此我们只需要对于每一段维护一个min{fj}即可。

法1.线段树

这个fj当然可以用线段树求区间min。因为单调队列中,只有队首和队尾的两个元素的min{fj}+hk是每次都要重算的,因此总复杂度O(nlogn)。用std::multiset维护单调队列中min{fj}+hk的值,在插入新元素时重算后投入multiset,弹出元素时扔出去即可。总复杂度O(nlogn)

代码:

#include<bits/stdc++.h>
using namespace std;
#define lson x<<1
#define rson x<<1|1
#define mid ((l+r)>>1)
#define int long long
int n,m,s[100100],h[100100],f[100100],q[100100],p[100100],l,r,mn[400100];
void pushup(int x){
	mn[x]=min(mn[lson],mn[rson]);
}
void Modify(int x,int l,int r,int P,int val){
	if(l>P||r<P)return;
	if(l==r){mn[x]=val;return;}
	Modify(lson,l,mid,P,val),Modify(rson,mid+1,r,P,val),pushup(x);
}
int Ask(int x,int l,int r,int L,int R){
	if(l>R||r<L)return 0x3f3f3f3f3f3f3f3f;
	if(L<=l&&r<=R)return mn[x];
	return min(Ask(lson,l,mid,L,R),Ask(rson,mid+1,r,L,R));
}
multiset<int>ms;
signed main(){
	scanf("%lld%lld",&n,&m),memset(f,0x3f,sizeof(f)),f[0]=0;
	for(int i=1;i<=n;i++)scanf("%lld%lld",&h[i],&s[i]),s[i]+=s[i-1];
	l=r=1,ms.insert(0);
	for(int i=1,j=1;i<=n;i++){
		Modify(1,1,n,i,f[i-1]);
		while(s[i]-s[j-1]>m)j++;
		while(l<=r&&s[i]-s[q[l]-1]>m)ms.erase(ms.find(p[l++]));
		while(l<=r&&h[i]>=h[q[r]])ms.erase(ms.find(p[r--]));
		q[++r]=i;
		if(l<r)p[r]=h[q[r]]+Ask(1,1,n,q[r-1]+1,q[r]),ms.erase(ms.find(p[l])),ms.insert(p[r]);
		p[l]=h[q[l]]+Ask(1,1,n,j,q[l]),ms.insert(p[l]);
		f[i]=*ms.begin();
	}
	printf("%lld\n",f[n]);
	return 0;
}

法2.直接观察

实际上,这个f是单调不降的(注意它的定义)。因此min{fj}一定出现在最左边的地方。因此直接用最左边的f值替换上面的min即可。因为复杂度瓶颈在multiset上,因此复杂度不变。

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,s[100100],h[100100],f[100100],q[100100],p[100100],l,r;
multiset<int>ms;
signed main(){
	scanf("%lld%lld",&n,&m),memset(f,0x3f,sizeof(f)),f[0]=0;
	for(int i=1;i<=n;i++)scanf("%lld%lld",&h[i],&s[i]),s[i]+=s[i-1];
	l=r=1,ms.insert(0);
	for(int i=1,j=1;i<=n;i++){
		while(s[i]-s[j-1]>m)j++;
		while(l<=r&&s[i]-s[q[l]-1]>m)ms.erase(ms.find(p[l++]));
		while(l<=r&&h[i]>=h[q[r]])ms.erase(ms.find(p[r--]));
		q[++r]=i;
		if(l<r)p[r]=h[q[r]]+f[q[r-1]],ms.erase(ms.find(p[l])),ms.insert(p[r]);
		p[l]=h[q[l]]+f[j-1],ms.insert(p[l]);
		f[i]=*ms.begin();
	}
	printf("%lld\n",f[n]);
	return 0;
}

V.CF295D Greg and Caves

题解

VI.[IOI2009]salesman

思想非常simple:因为一次从上游往下游的转移,可以被表示成

fi+(posiposj)×Ufj | posi<posjtimi<timj

拆开括号,即可得到两半互不相关的部分。然后直接使用线段树/树状数组进行转移即可。

从下游往上游的转移也可以类似地处理。

现在考虑tim中可能有相等的情形,并不能确定访问顺序。这个再使用一遍辅助DP过一遍就行了。有一个结论是当tim相等时,一次转移中一定不会走回头路——回头路的部分完全可以在上次转移和下次转移处就处理掉了。然后就直接DP过就行了。

3min就能想出的idea,我整整调了3d。主要因为一开始套了两重离散化,后来发现数据范围开的下便删去了离散化;一开始写的是线段树,后来发现线段树debug起来很麻烦,便换成了BIT;一开始也没有想到没有回头路的情形,辅助DP时写的极其憋屈(后来证明就是这个憋屈的DP中有一处UD写反了);同时中文题面翻译还翻译错了,这个“距离”是到上游的距离而非到下游的距离。于是种种因素叠加在一起,debug得精神崩溃。

代码:

#include<bits/stdc++.h>
using namespace std;
const int inf=0xc0c0c0c0;
const int N=1001000;
int n,U,D,S,tim[N],pos[N],bon[N],m=500100,ord[N],f[N],g[N],upper[N],lower[N];//upper:the maximal when go against the wave; lower:vice versa
void modify(int P,int val){
	for(int x=P;x;x-=x&-x)upper[x]=max(upper[x],val-P*U);
	for(int x=P;x<=m;x+=x&-x)lower[x]=max(lower[x],val+P*D);
}
int queryupper(int P){
	int ret=inf;
	for(int x=P;x<=m;x+=x&-x)ret=max(ret,upper[x]+P*U);
	return ret;
}
int querylower(int P){
	int ret=inf;
	for(int x=P;x;x-=x&-x)ret=max(ret,lower[x]-P*D);
	return ret;
}
#define I ord[i]
#define J ord[j]
#define K ord[k]
int main(){
	scanf("%d%d%d%d",&n,&U,&D,&S),memset(upper,0xc0,sizeof(upper)),memset(lower,0xc0,sizeof(lower));
	for(int i=1;i<=n;i++)scanf("%d%d%d",&tim[i],&pos[i],&bon[i]),ord[i]=i;
	sort(ord+1,ord+n+1,[](int x,int y){return tim[x]==tim[y]?pos[x]<pos[y]:tim[x]<tim[y];});
	modify(S,0);
	for(int i=1,j=1;j<=n;){
		while(tim[I]==tim[J])f[J]=g[J]=max(queryupper(pos[J]),querylower(pos[J]))+bon[J],j++;
		for(int k=i+1;k<j;k++)f[K]=max(f[K],f[ord[k-1]]-(pos[K]-pos[ord[k-1]])*D+bon[K]);
		for(int k=j-2;k>=i;k--)g[K]=max(g[K],g[ord[k+1]]-(pos[ord[k+1]]-pos[K])*U+bon[K]);
		while(i<j)modify(pos[I],max(f[I],g[I])),i++;
	}
	printf("%d\n",max(queryupper(S),querylower(S)));
	return 0;
}

VII.[JSOI2016]病毒感染

fi 表示当前在位置 ii 之前(不包括 i)的小镇的所有病人都被治好时,且已经决定放弃 i 去治后面人时死掉的最少人数。初始 f1=0,答案为 fn+1

然后思考路径,发现必然是从 i 出发走到位置 j,一路上治好几个小镇的人,然后掉过头来走到 i,治好第一趟中没治好的人。

考虑转移。显然我们首先可以选择治好 i,转移到 fi+1,代价为二倍的后缀病人数和。

我们也可以选择往后走到 j。对于路途中的每个小镇,我们有两种选择:

  • 放弃它。因为我们确定终点是 j,而到达这个小镇并治好它的时候直到终点的所有小镇必然已被治愈,所以这个决策的伤害是确定的。
  • 治好它。这对于之前放弃的小镇来说没有影响(因为它的伤害已经被计算了),故伤害是其之后的小镇的人数和。

对于每个小镇分开决策即可做到 n3

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,a[3010];
ll f[3010],s[3010];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=n;i;i--)s[i]=s[i+1]+a[i];
	memset(f,0x3f,sizeof(f)),f[1]=0;
	for(int i=1;i<=n;i++){
		f[i+1]=min(f[i+1],f[i]+(s[i+1]<<1));
		for(int j=i+1;j<=n;j++){
			ll val=f[i]+(3*(j-i)+(j-i+1)+1)*s[j+1]+3ll*(j-i)*a[i];
			for(int k=i+1;k<j;k++){
				val+=s[k]-s[j+1];
				ll A=3ll*(j-k)*a[k];
				ll B=s[k+1]-s[j+1];
				val+=min(A,B);
			}
			val+=a[j];
			f[j+1]=min(f[j+1],val);
		}
	}
	printf("%lld\n",f[n+1]);
	return 0;
}

考虑优化。发现上述代码中小镇的决策与 i 无关,那就预处理一下即可。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,a[3010];
ll f[3010],s[3010],g[3010][3010];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=n;i;i--)s[i]=s[i+1]+a[i];
	memset(f,0x3f,sizeof(f)),f[1]=0;
	for(int j=1;j<=n;j++)for(int k=1;k<j;k++){
		ll A=3ll*(j-k)*a[k];
		ll B=s[k+1]-s[j+1];
		g[j][k]=min(A,B)+s[k]-s[j+1];
		g[j][k]+=g[j][k-1];
	}
	for(int i=1;i<=n;i++){
		f[i+1]=min(f[i+1],f[i]+(s[i+1]<<1));
		for(int j=i+1;j<=n;j++){
			ll val=f[i]+(3*(j-i)+(j-i+1)+1)*s[j+1]+3ll*(j-i)*a[i]+(g[j][j-1]-g[j][i]);
			val+=a[j];
			f[j+1]=min(f[j+1],val);
		}
	}
	printf("%lld\n",f[n+1]);
	return 0;
}

III.状态设计类 DP

对于一些朴素的题目,状态设计完一切就结束了。

I.CF1067A Array Without Local Maximums

这题DEBUG的我心态爆炸……后来发现是一个i打成j了……无语。

很容易想到,设f[i][j][0/1]表示:

到第i位时,位置i填入了j,且j位置i-1上的数的状态是0/1的种数。

但这就会有问题:反过来是,而不是<

因此我们还要记录一下是否相等。即设f[i][j][0/1/2]表示:

到第i位时,位置i填入了j,且j相较于上一位是小于/等于/大于。

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=998244353;
const int lim=200;
int n,num[100100],f[2][210][3],res;
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&num[i]);
	if(num[1]==-1)for(int i=1;i<=lim;i++)f[1][i][0]=1;
	else f[1][num[1]][0]=1;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=lim;j++)f[i&1][j][0]=f[i&1][j][1]=f[i&1][j][2]=0;
		int s;
		s=0;
		for(int j=1;j<=lim;j++){
			if(num[i]==-1||j==num[i])f[i&1][j][0]=s;
			(s+=f[!(i&1)][j][0]+f[!(i&1)][j][1]+f[!(i&1)][j][2])%=mod;
		}
		for(int j=1;j<=lim;j++)if(num[i]==-1||j==num[i])f[i&1][j][1]=(f[!(i&1)][j][0]+f[!(i&1)][j][1]+f[!(i&1)][j][2])%mod;
		s=0;
		for(int j=lim;j>=1;j--){
			if(num[i]==-1||j==num[i])f[i&1][j][2]=s;
			(s+=f[!(i&1)][j][1]+f[!(i&1)][j][2])%=mod;
		}
//		printf("%d:",i);for(int j=1;j<=lim;j++)if(f[i&1][j][0]||f[i&1][j][1])printf("(%d:%d %d)",j,f[i&1][j][0],f[i&1][j][1]);puts("");
	}
	for(int i=1;i<=lim;i++)(res+=f[n&1][i][1]+f[n&1][i][2])%=mod;
	printf("%lld\n",res);
	return 0;
}

II.[USACO5.5]贰五语言Two Five

这题已经在我的收藏夹里面吃了大半年的灰了

发现当表格填到某个地方后,它一定是呈现出一条逐行递减的轮廓线的。

因此,我们设f[a][b][c][d][e]表示第1行填了a个……第5行填了e个的方案数。

则只有5abcde0的状态才是合法的。

用记忆化搜索实现。之后对于每一位确定应该填什么即可。复杂度257(上界,真实复杂度远远不到)

代码:

#include<bits/stdc++.h>
using namespace std;
int f[6][6][6][6][6],n;
char tp[2],s[100],t[100];
bool che(char x,int y){
	return (!x)||x=='A'+y;
}
int dfs(int a,int b,int c,int d,int e){
	if(a+b+c+d+e==25)return 1;
	int &ret=f[a][b][c][d][e];
	if(ret)return ret;
	if(a<5&&che(s[a],a+b+c+d+e))ret+=dfs(a+1,b,c,d,e);
	if(b<a&&che(s[b+5],a+b+c+d+e))ret+=dfs(a,b+1,c,d,e);
	if(c<b&&che(s[c+10],a+b+c+d+e))ret+=dfs(a,b,c+1,d,e);
	if(d<c&&che(s[d+15],a+b+c+d+e))ret+=dfs(a,b,c,d+1,e);
	if(e<d&&che(s[e+20],a+b+c+d+e))ret+=dfs(a,b,c,d,e+1);
//	printf("%d %d %d %d %d:%d\n",a,b,c,d,e,res);
	return ret;
}
bool used[100];
int main(){
	scanf("%s",tp);
	if(tp[0]=='N'){
		scanf("%d",&n);
		int sum=0;
		for(int i=0;i<25;i++)for(s[i]='A';s[i]<='Z';s[i]++){
			if(used[s[i]])continue;
			used[s[i]]=true;
			memset(f,0,sizeof(f));
			int tmp=dfs(0,0,0,0,0);
			if(sum+tmp>=n)break;
			sum+=tmp;
			used[s[i]]=false;
		}
		printf("%s\n",s);
	}else{
		scanf("%s",t);
		int res=0;
		for(int i=0;i<25;i++)for(s[i]='A';s[i]<t[i];s[i]++){
			memset(f,0,sizeof(f));
			res+=dfs(0,0,0,0,0); 
		}
		printf("%d\n",res+1);
	}
	return 0;
}

III.[九省联考2018]一双木棋chess

一下子就想到了上一题,因为同是阶梯型的图样。然后稍微想一想就发现总方案数可以用隔板法证得是(n+mm)的,代入一看发现才2×105都不到。于是就果断DP了。

首先先用爆搜搜出所有图案的分布(实现从编号到图案的映射),然后再预处理一个辅助的DP来实现从图案到编号的映射。然后就直接分当前是谁操作进行不同的转移即可。

时间复杂度,如上所述,是(n+mm)×转移复杂度的。我采取的转移是O(n2)的,还可以被优化为最终O(n)转移,但是没有必要。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,cnt,f[20][20],a[20][20],b[20][20],g[200000];
vector<int>v[200100];
vector<int>u;
void dfs(int pos,int lim){
	if(pos==n){v[++cnt]=u;return;}
	for(int i=0;i<=lim;i++){
		u.push_back(i);
		dfs(pos+1,i);
		u.pop_back();
	}
}
int deco(vector<int>&ip){
	int ret=1;
	for(int i=0;i<n;i++)if(ip[i])ret+=f[i][ip[i]-1];
	return ret;
}
int dp(int ip,bool sd){
	if(ip==cnt)return 0;
	if(g[ip]!=-1)return g[ip];
	if(sd==0){//first player
		g[ip]=0xc0c0c0c0;
		vector<int>tmp=v[ip];
		for(int i=0;i<n;i++){
			if(i==0&&tmp[i]==m||i>0&&tmp[i]==tmp[i-1])continue;
			tmp[i]++;
			g[ip]=max(g[ip],dp(deco(tmp),sd^1)+a[i][tmp[i]]);
			tmp[i]--;
		}
		return g[ip];
	}else{
		g[ip]=0x3f3f3f3f;
		vector<int>tmp=v[ip];
		for(int i=0;i<n;i++){
			if(i==0&&tmp[i]==m||i>0&&tmp[i]==tmp[i-1])continue;
			tmp[i]++;
			g[ip]=min(g[ip],dp(deco(tmp),sd^1)-b[i][tmp[i]]);
			tmp[i]--;
		}
		return g[ip];		
	}
}
int main(){
	scanf("%d%d",&n,&m),memset(g,-1,sizeof(g));
	dfs(0,m);
	for(int i=0;i<=m;i++)f[n-1][i]=i+1;
	for(int i=n-2;i>=0;i--)for(int j=0;j<=m;j++)for(int k=0;k<=j;k++)f[i][j]+=f[i+1][k];
	for(int i=0;i<n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);
	for(int i=0;i<n;i++)for(int j=1;j<=m;j++)scanf("%d",&b[i][j]);
	printf("%d\n",dp(1,0));
	return 0;
} 

IV.[GYM100134I][NEERC2012]Identification of Protein

debug5h,精神崩溃。

首先,很容易想到把所有东西都乘上 105 变成整数。然后,因为 gcd(9705276,12805858)=2,所以在字符串长度 400 时,每个值被表示成二者倍数之和的方式是唯一的。于是我们可以把所有合法的值唯一转换成“有 piPqiQ”的表达形式。因此,整条字符串中所具有的 P 数和 Q 数也就确定了。分别设其为 nm

然后,一条前缀就唯一对应了一条后缀,反之亦然。于是我们便可以建出一张矩阵,ai,j 表示有多少个串中有 iPjQ(当然,作为前缀和后缀)。此时,一条字符串就对应了一条 (0,0)(n,m) 的路径,而所有可以被表示成其前缀或后缀的值的数量就是路径经过所有位置的权值和。

但是,一个值如果同时作为前缀和后缀出现,是不能被计算两遍的。所以我们设计DP状态时,要同时记录下正着的和反着的位置,以在上述情况时及时判断出来。我们设 fi,j,k 表示当前填到前后缀各第 i 个字符,且前缀里已经填了 jP,后缀里已经填了 kQ 的最优收益。采取记忆化搜索进行DP,复杂度 O(n3)

要注意一堆细节,例如实际上当总串长度为奇或偶时,前后缀相遇处时的处理是不一样的。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int P=9705276;
const int Q=12805858;
const int inf=-0x3f3f3f3f;
int q,n,m,s,g[410][410],f[210][210][210],opt[210][210][210];
ll a[100100],mx;
double db;
pair<int,int>pr;
pair<int,int>pt(ll ip){
	for(int i=0;ip>=0;ip-=P,i++)if(!(ip%Q))return make_pair(i,ip/Q);
	return make_pair(-1,-1);
}
int dfs(int stp,int x1,int x2){
	int &now=f[stp][x1][x2];
	if(now!=-1)return now;
	if((x1+x2-(s&1)>n||stp-x1+stp-x2-(s&1)>m)||!(x1<=n&&x2<=n&&stp-x1<=m&&stp-x2<=m))return now=inf;
	now=g[x1][stp-x1];
	if(x1!=x2&&(x1!=n-x2||stp-x1!=m-(stp-x2)))now+=g[x2][stp-x2];
	if(stp==((s+1)>>1))return now;
	if(x1+x2>n||stp-x1+stp-x2>m)return now=inf;
	int A=dfs(stp+1,x1,x2);
	int B=((s&1)&&(stp==(s>>1)))?inf:dfs(stp+1,x1+1,x2);
	int C=((s&1)&&(stp==(s>>1)))?inf:dfs(stp+1,x1,x2+1);
	int D=dfs(stp+1,x1+1,x2+1);
	int M=max({A,B,C,D});
	now+=M;
	if(A==M)opt[stp][x1][x2]=1;
	else if(B==M)opt[stp][x1][x2]=2;
	else if(C==M)opt[stp][x1][x2]=3;
	else if(D==M)opt[stp][x1][x2]=4;
	return now;
}
char res[410];
int main(){
	freopen("identification.in","r",stdin);
	freopen("identification.out","w",stdout);
	scanf("%d",&q),memset(f,-1,sizeof(f));
	for(int i=1;i<=q;i++){
		scanf("%lf",&db);
		a[i]=db*100000+0.1;
		mx=max(mx,a[i]);
	}
	pr=pt(mx),n=pr.first,m=pr.second,s=n+m;
	for(int i=1;i<=q;i++){
		pr=pt(a[i]);
		if(pr==make_pair(-1,-1))continue;
		if(pr.first>n||pr.second>m)continue;
		g[pr.first][pr.second]++;
		if((pr.first<<1)!=n||(pr.second<<1)!=m)g[n-pr.first][m-pr.second]++;
	}
	dfs(0,0,0);
	for(int stp=0,x1=0,x2=0;stp+1<=s-stp;stp++){
		if(opt[stp][x1][x2]==1)res[stp+1]='Q',res[s-stp]='Q';
		else if(opt[stp][x1][x2]==2)res[stp+1]='P',res[s-stp]='Q',x1++;
		else if(opt[stp][x1][x2]==3)res[stp+1]='Q',res[s-stp]='P',x2++;
		else if(opt[stp][x1][x2]==4)res[stp+1]='P',res[s-stp]='P',x1++,x2++;
	}
	printf("%s\n",res+1);
	return 0;
}

V.CF612F Simba on the Circle

题解

VI.URAL1895. Steaks on Board

首先,一个DP状态是很好设计的,即 fi,j,k 表示 i 时刻,剩余 j 块牛排没烤,k 块牛排烤了一半时的最小答案。

明显,我们必定可以构造一种策略使得没烤的一定是当前所有牛排中最晚的 j 块,然后烤了一半的是牛排中次晚的 k 块。

一个 xn3 的做法是枚举当前时刻分多少位置烤没烤过的牛排,然后剩下的位置贪心地全部用来烤烤了一半的牛排。这个方法写起来非常非常简单。

一个 n5 的做法是将时刻离散化。

问题来了,为什么是 n5 而非 n4 呢?离散化不是只将 x 压缩到 n 了吗?

因为这个离散化会使得”剩下的位置贪心地全部用来烤烤了一半的牛排“这个结论失效,我们完全可以将一些牛排扔到将来,在同一时刻烤以节约时间。这就意味着我们需要枚举两维进行转移。

虽然 n5 看上去稍微有点慢(3×108),但是常数很小,因此可以通过。

但是那个 xn3 的算法要比这个好写很多!!!

代码(n5):

#include<bits/stdc++.h>
using namespace std;
int n,m,p,a[110],f[210][110][110];
pair<int,int>las[210][110][110];
vector<pair<int,int> >v;
void chmn(int i,int j,int k,int V,int J,int K){
//	printf("(%d,%d,%d)<-%d\n",i,j,k,V);
	if(f[i+1][j][k]<=V)return;
	f[i+1][j][k]=V,las[i+1][j][k]=make_pair(J,K);
}
int res[110][2];
deque<int>one,two;
vector<pair<int,int> >u;
int main(){
	scanf("%d%d%d",&p,&m,&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),v.emplace_back(max(a[i]-p,0),i),v.emplace_back(a[i],-i);
	sort(v.begin(),v.end()),memset(f,0x3f,sizeof(f)),f[0][0][0]=0;
	for(int i=0,pos=0,las=0;i+1<v.size();i++){
		if(v[i].second<0)las++;
		for(int j=0;j<=pos-las;j++)for(int k=0;j+k<=pos-las;k++){
//			printf("%d(%d),%d,%d:%d\n",i,v[i].first,j,k,f[i][j][k]);
			int J=j+(v[i].second>0);
			chmn(i,J,k,f[i][j][k],j,k);//do not use the stove
			if(v[i+1].first==v[i].first)continue;
			if(v[i+1].first==v[i].first+1)
				for(int l=max(0,J-m);l<=J;l++)for(int r=max(0,k+(J-l)-m)+(J-l);r<=k+J;r++)
					//reserve l steaks uncooked,r steaks half-cooked
					chmn(i,l,r,f[i][j][k]+1,j,k);
			else{
				int M=m*(v[i+1].first-v[i].first);
				for(int l=max(0,J-M);l<=J;l++)for(int r=max(0,k+2*(J-l)-M);r<=k+(J-l);r++)
					//reserve l steaks uncooked,r steaks half-cooked
					chmn(i,l,r,f[i][j][k]+max((2*(J-l)+(k-r)-1)/m+1,J-l&&r<(J-l)?2:1),j,k);
			}
		}
		if(v[i].second>0)pos++;
	}
	if(f[v.size()-1][0][0]>2*n){puts("-1");return 0;}
//	for(auto i:v)printf("(%d,%d)\n",i.first,i.second);
	printf("%d\n",f[v.size()-1][0][0]);
	for(int i=v.size()-1,j=0,k=0;i;i--){
		u.emplace_back(j,k);
		int J=las[i][j][k].first,K=las[i][j][k].second;
		j=J,k=K;
	}
	u.emplace_back(0,0);
	reverse(u.begin(),u.end());
	for(int i=0;i+1<u.size();i++){
		int j=u[i].first,k=u[i].second;
		int J=u[i+1].first,K=u[i+1].second;
		if(v[i].second>0)j++,two.push_back(v[i].second);
		int P=v[i].first-1,Q=m-1;
		while(j>J)res[two.front()][0]=(P+=!((++Q)%=m)),one.push_back(two.front()),two.pop_front(),j--,k++;
		while(k>K){
			res[one.front()][1]=(P+=!((++Q)%=m));
			if(P<=res[one.front()][0])res[one.front()][1]=P=res[one.front()][0]+1,Q=0;
			one.pop_front(),k--;
		}
	}
	for(int i=1;i<=n;i++)printf("%d %d\n",res[i][0],res[i][1]);
	return 0;
} 

VII.CF613E Puzzle Lover

因为是 2×n,所以“掉头”是很麻烦的,进而掉头的行为模式是简单的——其仅可能发生在整条路径的两端。

于是我们将整条路径切成两半,即起始的一个掉头和掉头结束后的一长段路径。DP 处理剩下的路径,然后枚举起始掉头将它们拼一块即可。除了结尾的掉头外的部分具有后效性,简单状压一下即可。而结尾的掉头直接枚举掉头长度即可。

细节贼多。记得翻转路径再做一遍,因为我们上述仅仅考虑向一个方向移动的路径。还要减去在两次统计中均被计算的路径——即没有向左向右移动过的路径。

时间复杂度 O(n2)

代码:

#include<bits/stdc++.h>
using namespace std;
const int M=3,N=4010,bs=37,md[M]={19260817,17680321,19491001};
int ksm(int x,int y,int mod){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int pov[M][N],inv[M][N];
struct HASH{
	int val[M];int len;
	HASH(){for(int i=0;i<M;i++)val[i]=0;len=0;}
	HASH(char ip){for(int i=0;i<M;i++)val[i]=ip-'a';len=1;}
	friend HASH operator+(const HASH&x,const HASH&y){
		HASH z;
		for(int i=0;i<M;i++)z.val[i]=(1ll*x.val[i]*pov[i][y.len]+y.val[i])%md[i];
		z.len=x.len+y.len;return z;
	}
	friend HASH operator/(const HASH&x,const HASH&y){
		HASH z;
		for(int i=0;i<M;i++)z.val[i]=1ll*(x.val[i]+md[i]-y.val[i])*inv[i][y.len]%md[i];
		z.len=x.len-y.len;return z;
	}
	friend bool operator==(const HASH&x,const HASH&y){
		if(x.len!=y.len)return false;
		for(int i=0;i<M;i++)if(x.val[i]!=y.val[i])return false;
		return true;
	}
	friend bool operator!=(const HASH&x,const HASH&y){return!(x==y);}
}S[2][2][2010],T[2010];
int n,m,f[2010][4][2010],mat[2][2][2010],res;
int g[2010][2][2010];
char s[2][2010],t[2010];
void init(){
	int lim=max(n,m);
	for(int t=0;t<M;t++){
		pov[t][0]=inv[t][0]=1;
		for(int i=1;i<=lim;i++)pov[t][i]=1ll*pov[t][i-1]*bs%md[t];
		inv[t][lim]=ksm(pov[t][lim],md[t]-2,md[t]);
		for(int i=lim;i;i--)inv[t][i-1]=1ll*inv[t][i]*bs%md[t];
	}
	for(int t=0;t<2;t++){
		for(int i=1;i<=n;i++)S[t][0][i]=s[t][i]+S[t][0][i-1];
		for(int i=n;i;i--)S[t][1][i]=s[t][i]+S[t][1][i+1];
	}
	for(int i=1;i<=m;i++)T[i]=t[i]+T[i-1];
}
const int mod=1e9+7;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
void solve(){
	memset(f,0,sizeof(f)),memset(g,0,sizeof(g));
	for(int i=n;i;i--)for(int j=3;j>=0;j--)for(int k=m;k;k--){
		if(s[j&1][i]!=t[k])continue;
		if(k==m){f[i][j][k]=1;if(m==1&&!(j&2))(++res)%=mod;continue;}
		if(!(j&2))ADD(f[i][j][k],f[i][3^j][k+1]);
		ADD(f[i][j][k],f[i+1][j&1][k+1]);
//			printf("%d,%d,%d:%d\n",i,j,k,f[i][j][k]);
		int p=m-k+1;
		if(!(j&2)&&p>=4&&!(p&1)&&i+(p>>1)-1<=n){
			bool ok=true;
			if(T[k+(p>>1)-1]/T[k-1]!=S[j][0][i+(p>>1)-1]/S[j][0][i-1])ok=false;
			if(T[m]/T[k+(p>>1)-1]!=S[!j][1][i]/S[!j][1][i+(p>>1)])ok=false;
			if(ok)(++f[i][j][k])%=mod;
		}
//		if(f[i][j][k])printf("%d,%d,%d:%d\n",i,j,k,f[i][j][k]);
		if(j&2)continue;
		if(k==1)ADD(res,f[i][j][k]);
		if(k>=5&&(k&1)){
			if(i<=(k>>1))continue;
			if(T[k-1]/T[k>>1]!=S[j&1][0][i-1]/S[j&1][0][i-1-(k>>1)])continue;
			if(T[k>>1]!=S[!(j&1)][1][i-(k>>1)]/S[!(j&1)][1][i])continue;
			ADD(res,f[i][j][k]);
		}
	}
}
void substract(){
	if(m==1)for(int i=1;i<=n;i++){
		if(s[0][i]==t[1])(res+=mod-1)%=mod;
		if(s[1][i]==t[1])(res+=mod-1)%=mod;
	}
	if(m==2)for(int i=1;i<=n;i++){
		if(s[0][i]==t[1]&&s[1][i]==t[2])(res+=mod-1)%=mod;
		if(s[1][i]==t[1]&&s[0][i]==t[2])(res+=mod-1)%=mod;
	}
}
int main(){
	scanf("%s%s%s",s[0]+1,s[1]+1,t+1),n=strlen(s[0]+1),m=strlen(t+1);
	init();
	solve();
	for(int t=0;t<2;t++){
		reverse(s[t]+1,s[t]+n+1);
		swap(S[t][0],S[t][1]),reverse(S[t][0]+1,S[t][0]+n+1),reverse(S[t][1]+1,S[t][1]+n+1);
	}
	solve();
	substract();
	printf("%d\n",res);
	return 0;
}

VIII.CF1007E Mini Metro

我们考虑描述某时刻 t 的状态:其必然有一段为 0 的前缀。设 p 为首个非 0 的位置。

考虑 上一个 p 上元素为 0 的时刻 s。我们只考虑 (s,t) 中所有时刻位置 p 上元素都非零的 (s,t) 对。

因为要修改 p+1 以后的时刻,前提条件是 p0;所以,我们得到结论是 (s,t) 中仅修改了前 p 个位置。

因为事实上每个时刻的变化都是相同的(位置 i 增加 bi),所以我们设 f(d,p) 来表示这个状态,也即间隔 ts=d 的状态。

考虑我们需要求出这个状态的什么信息:即为 要想这个状态中所有时刻都合法,至少需要派出多少趟车。为了符合定义,我们额外加一个限制就是 派出的车不得影响 p 之后的位置。可以发现,按照这个定义,可能会因为车的功能太强大导致 如果要合法,则必然会影响 p 之后的位置 的后果,此时定义其值为

考虑在 [0,d) 中所有时刻中最后一个影响 p 位置的时刻。假如其不存在,则所有变化都发生在 d 时刻。令 Bb 的前缀和,则总修改次数为 v=d×Bp1K 次。若 vK>d×Bp,则意味着这么做必然会影响到 p 以后的位置,进而此种转移不合法。或者,如果 dbp>cp,也意味着不合法。

假如其存在,则令之为 r。则在 r 时刻时,前 p1 个位置都是 0。这已经可以用一个状态来刻画了,也即 f(r,p)

考虑 r 时刻时 p 处的元素大小,为 rBpf(r,p)K。令 u 表示之。

r 时刻时,我们要派出若干额外的车,使得 rd 时刻前不会出问题。这一数量为 v=max(u+(dr)bpcp,0)K。假如 vK>u 这意味着转移不可避免地要影响 p 后的位置,也即不合法。

然后考虑 [r,d] 间的操作。因为最终 [r,d] 肯定要清零,所以派出 (sr)Bp1K 辆车即可。

但是,仅靠 f 没法解决问题。我们还要设辅助 DP 数组 g 表示在 t 时刻前 p 个位置符合条件 但不一定非空 的态,用来表现结束的样子;还要专门设一个 0/1 维来表示其是否是起始态(以考虑 a 的影响)。不过这是 trival 的。

复杂度三方。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf=0x3f3f3f3f3f3f3f3f;
int n,m,K;
ll f[210][210][2],g[210][210][2],A[210],B[210],C[210],a[210],b[210],c[210];
ll cl(ll x){return x<=0?0:(x-1)/K+1;}
int main(){
	scanf("%d%d%d",&n,&m,&K);
	for(int i=1;i<=n;i++)scanf("%lld%lld%lld",&a[i],&b[i],&c[i]);
	a[n+1]=inf>>1,b[n+1]=inf>>9,c[n+1]=inf;
	for(int i=1;i<=n+1;i++)A[i]=A[i-1]+a[i],B[i]=B[i-1]+b[i],C[i]=C[i-1]+c[i];
	for(int p=1;p<=n+1;p++)for(int d=0;d<=m;d++)for(int z=0;z<2;z++){
		f[p][d][z]=g[p][d][z]=inf;
		if(g[p-1][d][z]!=inf&&1ll*d*b[p]+z*a[p]<=c[p]){
			g[p][d][z]=g[p-1][d][z];
			ll v=cl(z*A[p-1]+d*B[p-1]);
			if(v*K<=z*A[p]+d*B[p])f[p][d][z]=v;
		}
		for(int r=0;r<d;r++){
			if(f[p][r][z]==inf)continue;
			ll u=z*A[p]+r*B[p]-f[p][r][z]*K;
			ll v=cl(max(u+(d-r)*b[p]-c[p],0ll));
//			printf("%d:%lld %lld\n",r,u,v);
			if(v*K>u||g[p-1][d-r][0]==inf)continue;
			g[p][d][z]=min(g[p][d][z],f[p][r][z]+v+g[p-1][d-r][0]);
			ll w=cl((d-r)*B[p-1]);
			if(w*K>(d-r)*B[p]+u)continue;
			f[p][d][z]=min(f[p][d][z],f[p][r][z]+v+w);
		}
//		printf("%d,%d,%d:%lld,%lld\n",p,d,z,f[p][d][z],g[p][d][z]);
	}
	printf("%lld\n",g[n+1][m][1]);
	return 0;
}

IX.[国家集训队]文学

这个问题乍一看非常奇怪——你找不到一个合适的描述其的模型。事实上,能描述其的是一个”最小覆盖问题“,即给定若干集合及其代价,要求所有集合的并为全集,并且最小化代价。而最小覆盖问题事实上是一个 NP Hard 问题进而这不是一个好的抽象。

我们考虑任意时刻你选出的东西是若干 半平面的并。但是半平面的并不具有任何良好的性质。

考虑取补集。补集仍然是半平面,且并的补集等于补集的交。于是我们的要求是补集的半平面交中不包含任何给定的点。

半平面交的性质之一就是它是 凸多边形。凸多边形的常见做法就是按照 极角排序 后,依照此种次序进行 DP。

我们考虑枚举两条直线找到其交点,并且钦定这个点作为交凸包的最下点(如有多个,取最左点)。这样开始 DP,设 fx,y 表示凸包上前两条直线分别是 x,y 时的最小答案,转移枚举下一条直线,我们得到了一个三方的 DP。外层还要再枚举两条直线,复杂度五方。

更好的做法是,注意到半平面交上每条直线相当于“阻挡”了若干点到被钦定点的连线,最终要求所有点的连线都被阻挡。

显然每条直线阻挡的是极角排序中的一段区间。于是我们一个思路是设 wl,r 表示覆盖极角序中区间 [l,r] 中所有点所需的最小半平面。枚举 l,r 以及覆盖之的半平面,我们可以在三方时间内完成 w 的求出,然后在平方的时间内由 w DP 出覆盖所有点需要的最小代价。

现在的复杂度仍然是五方,但是这个算法明显较于之前更加优异。

我们考虑枚举半平面,然后找到所有其能覆盖的极长极角序区间并得到一个粗略的 w,然后在 w 上区间 DP 一下就得到了精确的 w。这样复杂度就变成了四方。

代码:

#include<bits/stdc++.h>
using namespace std;
const double eps=1e-8;
typedef long long ll;
struct Vector{
	double x,y;
	Vector(){}
	Vector(double X,double Y){x=X,y=Y;}
	friend Vector operator-(const Vector&u,const Vector&v){return Vector(u.x-v.x,u.y-v.y);}
	friend double operator&(const Vector&u,const Vector&v){return u.x*v.y-u.y*v.x;}
	void read(){scanf("%lf%lf",&x,&y);}
}p[110],q[110],O;
typedef Vector Point;
struct Line{
	int a,b,c;
	friend bool operator&(const Line&u,const Vector&v){return 1ll*v.x*u.a+1ll*v.y*u.b<=u.c+eps;}
	friend Point operator&(const Line&u,const Line&v){
		double x,y;
		if(!u.a)y=1.0*u.c/v.b,x=(v.c-v.b*y)/v.a;
		else y=(1.0*v.a/u.a*u.c-v.c)/(1.0*v.a/u.a*u.b-v.b),x=(u.c-u.b*y)/u.a;
		return Vector(x,y);
	}
	void read(){scanf("%d%d%d",&a,&b,&c);}
}l[110];
bool operator<(const Vector&u,const Vector&v){return((u-O)&(v-O))>=0;}
int n,m,r,w[110];
int c[110][110],f[110],res=0x3f3f3f3f;
void chmn(int&x,int y){if(x>y)x=y;}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)l[i].read(),scanf("%d",&w[i]);
	for(int i=1;i<=m;i++)p[i].read();
	for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++){
		O=l[i]&l[j],r=0;
		for(int k=1;k<=m;k++)if(!(l[i]&p[k])&&!(l[j]&p[k]))q[r++]=p[k];
		sort(q,q+r),memset(c,0x3f,sizeof(c));
		for(int k=1;k<=n;k++)for(int L=0,R=0;;L=R){
			while(L<r&&!(l[k]&q[L]))L++;
			if(L==r)break;
			R=L;while(R<r&&(l[k]&q[R]))R++;
			chmn(c[L][R-1],w[k]);
		}
		for(int L=0;L<r;L++)for(int R=r-1;R>L;R--)
			chmn(c[L+1][R],c[L][R]),chmn(c[L][R-1],c[L][R]);
		memset(f,0x3f,sizeof(f)),f[0]=0;
		for(int L=0;L<r;L++)for(int R=L;R<r;R++)chmn(f[R+1],f[L]+c[L][R]);
//		printf("[%d,%d]:%d,%d,%d\n",i,j,f[r],w[i],w[j]);
		chmn(res,f[r]+w[i]+w[j]);
	}
	for(int i=1;i<=n;i++){
		bool ok=true;
		for(int j=1;j<=m;j++)ok&=(l[i]&p[j]);
		if(ok)chmn(res,w[i]);
	}
	printf("%d\n",res==0x3f3f3f3f?-1:res);
	return 0;
}

X.ddtt

给定一张无向图,你要为边定向,在向两种方向定向时各自有不同的费用。

你的目标是最终定向后形成的有向图强连通。输出最小费用。若不存在合法方案,输出 1

数据范围:n18

首先方法一是模拟退火:为一个状态赋上的权值是其中强连通分量数乘以 再加上总费用。调参调得好就能过。

然后方法二就是状压了。

这要求我们找到一种好的描述 SCC 结构的方案。

我们可以把 SCC 抽象成 在一个 SCC 上并入一条路径,满足路径的起讫点都在原始 SCC 中。初始令 SCC 为单点。这个状态显然是正确的。

考虑转移。一种方案是预处理出 (u,v,S) 的所有态,表示一条从 uv 经过 S 中所有节点需要的最小代价。转移需要枚举补集转移,复杂度 3nn2

我们考虑把这个东西也记录到状态中,即令 fS,w,v 表示初始的 S 并上路径已经过的部分(即 uw 的部分)是 S,路径当前已经走到了节点 w,目标是节点 v,此时的最小代价。

特别需要注意的是,并非所有边都是维持 DAG 结构的关键边,有些边是无论怎么定向都可以的。为了统计这些边的影响,我们初始把所有边强制定向为费用较小的方向,然后问题转换为我们可以支付若干额外的费用,翻转一条边的方向。这时,被翻转的边就全是维持 DAG 结构的关键边,我们的 DP 就正确了。

另外需要注意的是,对于一条路径 uv,若其中间仅经过一个点 w,则其不能成环(也即 uv)。解决方法就是对于 u=v 的路径,初始暴力枚举两步转移。因为 u=v 的态仅有 O(n) 个,所以复杂度仍是正确的。

转移枚举后继边,复杂度 O(2nn3)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[18][18],f[1<<18][18][18],g[1<<18],sum;
void chmn(int&x,int y){if(x>y)x=y;}
int main(){
	freopen("ddtt.in","r",stdin);
	freopen("ddtt.out","w",stdout);
	scanf("%d",&n);
	for(int i=0;i<n;i++)for(int j=0;j<n;j++)scanf("%d",&a[i][j]);
	for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)if(a[i][j]!=-1&&a[j][i]!=-1){
		int val=min(a[i][j],a[j][i]);
		sum+=val,a[i][j]-=val,a[j][i]-=val;
	}
	memset(f,0x3f,sizeof(f)),memset(g,0x3f,sizeof(g)),g[1]=0;
	for(int i=1;i<(1<<n);i++){
		for(int j=0;j<n;j++)for(int k=0;k<n;k++)if(f[i][j][k]!=0x3f3f3f3f&&j!=k)
			for(int w=0;w<n;w++)if((!(i&(1<<w))||w==k)&&a[j][w]!=-1)
				chmn(f[i|(1<<w)][w][k],f[i][j][k]+a[j][w]);
		for(int j=0;j<n;j++)if(f[i][j][j]!=0x3f3f3f3f)chmn(g[i],f[i][j][j]);
		if(g[i]==0x3f3f3f3f)continue;
		// printf("%d:%d\n",i,g[i]);
		for(int u=0;u<n;u++)if(i&(1<<u))for(int v=0;v<n;v++)if(i&(1<<v))
			if(u!=v){
				for(int w=0;w<n;w++)if(!(i&(1<<w))&&a[u][w]!=-1)chmn(f[i|(1<<w)][w][v],g[i]+a[u][w]);
			}else{
				for(int w1=0;w1<n;w1++)if(!(i&(1<<w1))&&a[u][w1]!=-1)
				for(int w2=0;w2<n;w2++)if(!(i&(1<<w2))&&a[w1][w2]!=-1&&w2!=v)
					// printf("<%d,%d>\n",w1,w2),
					chmn(f[i|(1<<w1)|(1<<w2)][w2][v],g[i]+a[u][w1]+a[w1][w2]);
			}
	}
	if(g[(1<<n)-1]==0x3f3f3f3f)puts("-1");else printf("%d\n",g[(1<<n)-1]+sum);
	return 0;
}

XI.CF251E Tree and Table

一种想法是,设 fx,i,j 表示节点 x 的子树在一个第一行有 i 个空位、第二行有 j 个空位(保证 i+j 等于子树大小),两行右对齐的网格的左上角,然后开 D!

有问题。首先是状态数略微有那么一点点多(可以通过一些类似跳单儿子链的操作减少,但是削减完后还是不好分析),二是细节略微有那么一点点多。

所以我们重定义状态,仅考虑 i=j 的态。于是我们令 fx 表示 x 的子树在一个 2×szx2 的矩阵(保证 szx 为偶数)的左上角的方案数,gx,y 表示 x,y2×szx+szy2(同理保证偶数)的左侧两格子的方案数。

首先考虑初态。为了避免重复讨论,我们特判链的场合(2n22n+4)后,定三度点为根,枚举三度点的位置,用 next_permutation 分配三个儿子的位置,即可用 f,g 表示初态。

考虑转移。首先 g 的转移是简单的:

  • 如果 x,y 有一度数为二,答案为 0
  • 有一度数为零,转移到度数非零者的儿子的 f
  • 均为一,则两个各跳一步儿子到 g

考虑 f。设当前点为 x

  • 如果度数为零,这个 f 不存在。

  • 如果度数为一:令儿子为 y

    • y 放在 x 下方。如果 y 有两个儿子,则该情形不合法,否则如果 y 无儿子,贡献为 1y 有一个儿子,转移到该儿子的 f

    • y 放在 x 右方。令 Yy 子树中首个二度点,则唯二合法流程为:

      xyyyyyyYu
      Vvvvvvvv(r)
      
      xyyyyyy
      VvvvvvYu
      

      其中,yyyYy 开始的一度链,vvvVY 的某个儿子 v 开始的一度链。仅有这两条一度链的长度满足上述要求时,才会合法,转移到 fugu,r,其中 r 为可能存在的 v 的另一个儿子。

  • 如果度数为二:令两个儿子 y,z。则一个放右、一个放下,下方儿子要么一度(此时转移到儿子的 g),要么零度(此时转移到另一个儿子的 f)。

虽然状态数看似爆炸,但其实并没有爆炸。可以记忆化。

细节很多。

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n;
vector<int>v[200100];
int sz[200100],dep[200100],jum[200100];
void dfs_init(int x,int fa){
	sz[x]=1,dep[x]=dep[fa]+1;
	jum[x]=x;
	for(auto y:v[x]){
		v[y].erase(find(v[y].begin(),v[y].end(),x));
		dfs_init(y,x),sz[x]+=sz[y];
		if(v[x].size()==1)jum[x]=jum[y];
	}
}
map<int,int>f;
map<pair<int,int>,int>g;
int F(int);
int G(int,int);
int F(int x){
	if(f.find(x)!=f.end())return f[x];
	int&ret=f[x];
	if(v[jum[x]].empty())return ret=(sz[x]>>1);
	if(v[x].size()==2){
		int y=v[x][0],z=v[x][1];
		auto func=[&](){
			if(v[z].empty())(ret+=F(y))%=mod;
			else if(v[z].size()==1)(ret+=G(y,v[z][0]))%=mod;
		};
		func();swap(y,z);func();
		return ret;
	}
	if(jum[x]!=v[x][0])(ret+=F(v[v[x][0]][0]))%=mod;
	int X=jum[x];
	int p=v[X][0],q=v[X][1];
	auto func=[&](){
		int P=jum[p];
		if(v[P].empty()&&dep[P]-dep[p]==dep[X]-dep[x]-2)(ret+=F(q))%=mod;
		if(v[P].empty()&&dep[P]-dep[p]==dep[X]-dep[x])(ret+=F(q))%=mod;
		if(v[p].size()==2){
			int a=v[p][0],b=v[p][1];
			auto cunf=[&](){
				int A=jum[a];
				if(v[A].empty()&&dep[A]-dep[a]+1==dep[X]-dep[x])(ret+=G(b,q))%=mod;
			};
			cunf(),swap(a,b),cunf();
		}
	};
	func();swap(p,q),func();
	return ret;
}
int G(int x,int y){
	if(x>y)swap(x,y);
	if(g.find(make_pair(x,y))!=g.end())return g[make_pair(x,y)];
	int&ret=g[make_pair(x,y)];
	if(v[x].size()==2||v[y].size()==2)return ret=0;
	if(v[x].empty()&&v[y].empty())return ret=1;
	if(v[x].empty())return ret=F(v[y][0]);
	if(v[y].empty())return ret=F(v[x][0]);
	return ret=G(v[x][0],v[y][0]);
}
int res;
int main(){
	scanf("%d",&n),n<<=1;
	for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
	for(int i=1;i<=n;i++)if(v[i].size()>3){puts("0");return 0;}
	int rt=-1;
	for(int i=1;i<=n;i++)if(v[i].size()==3)rt=i;
	n>>=1;
	if(rt==-1){printf("%d\n",n==1?2:(2ll*n*n-2*n+4)%mod);return 0;}
	dfs_init(rt,0);
	sort(v[rt].begin(),v[rt].end());
	for(int i=2;i<n;i++)do{
		int x=v[rt][0],y=v[rt][1],z=v[rt][2];
		if(v[z].empty()){
			if(sz[x]!=((i-1)<<1)||sz[y]!=((n-i)<<1))continue;
			res=(1ll*F(x)*F(y)+res)%mod;
			continue;
		}
		int p=v[z][0],q=(v[z].size()==1?0:v[z][1]);
		auto func=[&](){
			if(sz[x]+sz[p]==((i-1)<<1)&&sz[y]+sz[q]==((n-i)<<1))
				res=(1ll*(p?G(x,p):F(x))*(q?G(y,q):F(y))+res)%mod;
		};func(),swap(p,q),func();
	}while(next_permutation(v[rt].begin(),v[rt].end()));
	(res<<=1)%=mod;
	printf("%d\n",res);
	return 0;
}
posted @   Troverld  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示