Typesetting math: 13%

复杂分析类 DP 学习笔记

主要涉及到问题本身的分析与变化的题目,往往思维难度高,且实现难度也不一定就很低。

I.处理类 DP

对于一类 DP,我们要对于数据本身进行某些处理,才能构建 DP。

I.[SHOI2007]书柜的尺寸

排序是各类DP题中只要出现了物品这个意象后的常客。

我们首先将书按照高度递减排序。这样,一个书柜的高度,就是第一本被放进来的书的高度。

f[i][j][k]f[i][j][k]表示:DP到第ii本书,第一层书架的长度为jj,第二层书架的长度为kk时,整个书柜的最小高度。设sum[i]sum[i]表示所有书厚度的前缀和。

我们枚举这本书到底是放入第一层、第二层还是第三层(第三层长度为sum[i]jksum[i]jk)。

转移分几种情况:

  1. j=0j=0,即第一层为空时,应该加上h[i]h[i]

  2. k=0k=0,应该加上h[i]h[i]

  3. j+k=sum[i]j+k=sum[i],应该加上h[i]h[i]

答案就是枚举f[n][?][?]f[n][?][?]统计答案即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int f[2][2110][2110],n,sum[110];
typedef long long ll;
ll res=0x3f3f3f3f3f3f3f3f;
pair<int,int>p[100];
bool cmp(pair<int,int>x,pair<int,int>y){
	return x.first>y.first;
}
int main(){
	scanf("%d",&n),memset(f,0x3f3f3f3f,sizeof(f));
	for(int i=1;i<=n;i++)scanf("%d%d",&p[i].first,&p[i].second);
	sort(p+1,p+n+1,cmp);
	for(int i=1;i<=n;i++)sum[i]=sum[i-1]+p[i].second;
	f[0][0][0]=0;
	for(int i=0;i<n;i++){
		memset(f[!(i&1)],0x3f3f3f3f,sizeof(f[!(i&1)]));
		for(int j=0;j<=sum[i];j++)for(int k=0;j+k<=sum[i];k++){
			f[!(i&1)][j+p[i+1].second][k]=min(f[!(i&1)][j+p[i+1].second][k],f[i&1][j][k]+p[i+1].first*(!j));
			f[!(i&1)][j][k+p[i+1].second]=min(f[!(i&1)][j][k+p[i+1].second],f[i&1][j][k]+p[i+1].first*(!k));
			f[!(i&1)][j][k]=min(f[!(i&1)][j][k],f[i&1][j][k]+p[i+1].first*(!(sum[i]-j-k)));
		}
	}
	for(int i=1;i<sum[n];i++)for(int j=1;i+j<sum[n];j++)res=min(res,1ll*max(max(i,j),sum[n]-i-j)*f[n&1][i][j]);
	printf("%d\n",res);
	return 0;
}

II.[JSOI2009]火星藏宝图

一个非常显然的结论:在最优方案中,路径上的任意两个点所构成的矩形内部一定不存在其它点。不然的化,在这个其它的点多停留一下一定不会更差。

因为a2+b2<(a+b)2a2+b2<(a+b)2

但是,就算想到这个,我也得不出什么好的转移方式

考虑将所有岛屿按照行优先,如果行相同就按列优先进行排序。这样,对于任何一个岛ii,所有编号小于ii的且列比它小的岛都是可转移的。

而在所有列相同的岛中,行最大的那个一定是最优的。

因此我们可以针对每行维护一个列数最大的点(类似于桶),每次只需要遍历这些桶进行转移即可。

复杂度O(nm)O(nm),卡卡就卡过去了。

代码:

#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
int n,m,tri[1010],f[200100];
struct node{
	int x,y,v;
	friend bool operator <(const node &x,const node &y){
		if(x.x!=y.x)return x.x<y.x;
		return x.y<y.y;
	}
}is[200100];
inline void read(int &x){
	x=0;
	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<0)putchar('-'),x=-x;
	if(x<=9)putchar('0'+x);
	else print(x/10),putchar('0'+x%10);
}
int main(){
	read(n),read(m);
	for(register int i=1;i<=n;i++)read(is[i].x),read(is[i].y),read(is[i].v);
	sort(is+1,is+n+1);
	tri[1]=1;
	f[1]=is[1].v;
	for(register int i=2;i<=n;i++){
		f[i]=f[1]-(is[i].x-1)*(is[i].x-1)-(is[i].y-1)*(is[i].y-1);
		for(register int j=1;j<=is[i].y;j++)if(tri[j])f[i]=max(f[i],f[tri[j]]-(is[i].x-is[tri[j]].x)*(is[i].x-is[tri[j]].x)-(is[i].y-is[tri[j]].y)*(is[i].y-is[tri[j]].y));
		f[i]+=is[i].v;
		tri[is[i].y]=i;
	}
	print(f[n]);
	return 0;
}

III.[HAOI2006]数字序列

第一问:

正难则反。我们考虑从这个序列中找出最多可以保留的数。

如果两个下标i,j(i<j)i,j(i<j)都是要保留的,那么保留的充要条件就是

ajaijiajaiji

因为(i,j)(i,j)开区间中的其它数要保证仍然有可以修改到的位置。例如 10 4 3 12 这组数据中,1012便不能同时选择,因为43要有修改的位置。

上面式子调换一下,便是

ajjaiiajjaii

aii=biaii=bi,则

bjbibjbi便是充要条件。

当我们找出了完整的bb数组后,发现bb中的最长不降子序列便是可以保留的位置。

然后第一问答案就是nb中LIS的长度nbLIS

第二问:

实际上就是让bb中LIS的长度为nn

考虑在第一问中找出的一条LIS上修改(注意LIS很有可能不止一条)。

我们在LIS中找出相邻的两个下标j,i(j<i)j,i(j<i)

则一组合法的修改结果肯定是一个“台阶”形。

我们发现,对于一段“台阶”:

如果它向下的箭头比向上的箭头要多,那么台阶上移一定不会更劣。反之亦然。

比如上图最左端那级台阶,就是上移下移都可以;中间的台阶,向上移更优;右面的台阶,向下移最好。

这样我们就可以构思出一种移台阶的方法:

对于一段台阶,如果它向上最好,则一直向上移直到和右边的下一段台阶齐平。然后合并两段台阶,再对新生成的这段台阶进行类似的操作。这种操作一定不会使答案变差。

则全部移完后,我们发现原本一小段一小段的台阶,要么同左边合并了,要么同右边合并了,反正最后一定是左边与左端点有一段台阶,右边与右端点有一段台阶。我们可以枚举左右台阶间的断点取minmin

这样我们就可以DP了。设fifi表示以ii结尾的LIS的长度,gigi表示将[1,i][1,i]中所有台阶全都移完,且ii是某条LIS中的一个位置时的最小代价。显然,gigi能从所有j<i,bj<bi,fj=fi1j<i,bj<bi,fj=fi1jj转移过来。如果令b0=,bn+1=+,f0=0,fn+1=nmaxi=1{fi}+1b0=,bn+1=+,f0=0,fn+1=nmaxi=1{fi}+1的话,则答案为gn+1gn+1

在极端数据下,这种暴力转移复杂度是O(n3)O(n3)的(枚举iiO(n)O(n)的,枚举jj在极端数据下是O(n)O(n)的,枚举断点也是O(n)O(n)的)。但是,“数据随机”让这个算法的期望复杂度优化成了O(nlog2n)O(nlog2n),轻松通过。

代码:

#include<bits/stdc++.h>
using namespace std;
const int INF=1e6;
int n,a[50100],b[50100],f[50100],t[50100],lim,mx,g[50100],pre[50100],suf[50100];
vector<int>v;
void add(int x,int val){
	while(x<=lim)t[x]=max(t[x],val),x+=x&-x;
}
int ask(int x){
	int qwq=0;
	while(x)qwq=max(qwq,t[x]),x-=x&-x;
	return qwq;
}
vector<int>q[50100];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)b[i]=a[i]-i,v.push_back(b[i]);
	sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin()),lim=v.size();
	for(int i=1;i<=n;i++)b[i]=lower_bound(v.begin(),v.end(),b[i])-v.begin()+1;
	for(int i=1;i<=n;i++)f[i]=ask(b[i])+1,add(b[i],f[i]),mx=max(mx,f[i]);
	for(int i=1;i<=n;i++)b[i]=a[i]-i;
	printf("%d\n",n-mx);
	q[0].push_back(0);
	memset(g,0x3f3f3f3f,sizeof(g));
	b[0]=-INF;
	b[n+1]=INF;
	g[0]=0;
	f[n+1]=mx+1;
	for(int i=1;i<=n+1;i++){
		q[f[i]].push_back(i);
		for(auto j:q[f[i]-1]){
			if(b[j]>b[i])continue;
//			printf("%d %d:\n",j,i);
			pre[j]=suf[i]=0;
			for(int k=j+1;k<i;k++)pre[k]=pre[k-1]+abs(b[k]-b[j]);
			for(int k=i-1;k>j;k--)suf[k]=suf[k+1]+abs(b[k]-b[i]);
//			for(int k=j;k<i;k++)printf("%d ",pre[k]);puts("");
//			for(int k=j+1;k<=i;k++)printf("%d ",suf[k]);puts("");
			for(int k=j;k<i;k++)g[i]=min(g[i],g[j]+pre[k]+suf[k+1]);
		}		
	}
//	for(int i=0;i<=n+1;i++)printf("%9d ",f[i]);puts("");
//	for(int i=0;i<=n+1;i++)printf("%9d ",b[i]);puts("");
//	for(int i=0;i<=n+1;i++)printf("%9d ",g[i]);puts("");
	printf("%d\n",g[n+1]);
	return 0;
} 

IV.[SDOI2008]Sue的小球

DP做多了,手感自然就出来了。

话说这题打着“小球”的名字题目中却是“彩蛋”是怎么回事

首先,这个下落速度vv,尽管题面中说它可能为负数,但我们想一想,这可能吗?如果是负数答案就是正无穷(可以等着这个球一直向上飞),因此排除球速为负的可能。

如果是这样的话,那么,当我们经过一个球时,随手将它射爆明显是更好的行为。因此,无论何时,我们球已经被射下的位置一定是一个包含起始点x0x0的区间。

我们将所有球按照位置在x0x0左边还是右边压进两个vector中(下标从00开始)。在左边的vector(设为v1v1)中,我们按照xx值从大到小排序并处理;在右边(设为v2v2),我们从小到大排序。同时,我们在v1v1v2v2中都压入一个x=x0x=x0的球,方便初始化。

我们设f[i][j][0/1]f[i][j][0/1]表示:当前进行到v1v1的第ii位,v2v2的第jj位,同时位于这个区间的左/右端点的情况。我们可以提前计算出所有小球初始yy值的和,这样我们只需要最小化捡球过程中球下落的距离即可。

我们设s[i][j]s[i][j]表示:除了v1v1ii位和v2v2jj位外,其它球1s1s内下落的距离之和(这借鉴了任务安排费用提前计算的经典思想)。在实现中,这个可以直接通过前缀和做出。设x1[i]x1[i]表示v1v1xx值,x2[j]x2[j]表示v2v2xx值。

我们有

f[i][j][0]=f[i1][j][0]+|x1[i1]x1[i]|(s[i1][j])f[i][j][0]=f[i1][j][0]+x1[i1]x1[i](s[i1][j])

f[i][j][1]=f[i][j1][0]+|x2[j1]x2[j]|(s[i][j1])f[i][j][1]=f[i][j1][0]+x2[j1]x2[j](s[i][j1])

然后因为两边可以互相走,所以还有

f[i][j][0]=f[i][j][1]+|x2[j]x1[i]|s[i][j]f[i][j][0]=f[i][j][1]+x2[j]x1[i]s[i][j]

f[i][j][1]=f[i][j][0]+|x1[i]x2[j]|s[i][j]f[i][j][1]=f[i][j][0]+x1[i]x2[j]s[i][j]

两种转移取minmin即可。

复杂度O(n2)O(n2)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,X,s1[1010],s2[1010],sy,f[1010][1010][2],sv;
struct node{
	int x,y,v;
	node(int a=0,int b=0,int c=0){x=a,y=b,v=c;}
}b[1010];
vector<node>v1,v2;
bool cmp1(const node &x,const node &y){
	return x.x<y.x;
}
bool cmp2(const node &x,const node &y){
	return x.x>y.x;
}
int main(){
	scanf("%d%d",&n,&X),memset(f,0x3f3f3f3f,sizeof(f));
	for(int i=1;i<=n;i++)scanf("%d",&b[i].x);
	for(int i=1;i<=n;i++)scanf("%d",&b[i].y),sy+=b[i].y;
	for(int i=1;i<=n;i++)scanf("%d",&b[i].v);
	for(int i=1;i<=n;i++){
		if(b[i].x<X)v1.push_back(b[i]);
		if(b[i].x>X)v2.push_back(b[i]);
	}
	v1.push_back(node(X,0,0)),v2.push_back(node(X,0,0));
	sort(v1.begin(),v1.end(),cmp2);
	sort(v2.begin(),v2.end(),cmp1);
	for(int i=1;i<v1.size();i++)s1[i]=s1[i-1]+v1[i].v;
	for(int i=1;i<v2.size();i++)s2[i]=s2[i-1]+v2[i].v;
//	for(int i=0;i<v1.size();i++)printf("%d %d %d\n",v1[i].x,v1[i].y,v1[i].v);puts("");
//	for(int i=0;i<v2.size();i++)printf("%d %d %d\n",v2[i].x,v2[i].y,v2[i].v);puts("");
	sv=s1[v1.size()-1]+s2[v2.size()-1];
	f[0][0][0]=f[0][0][1]=0;
	for(int i=0;i<v1.size();i++)for(int j=0;j<v2.size();j++){
		if(i)f[i][j][0]=f[i-1][j][0]+abs(v1[i].x-v1[i-1].x)*(sv-s1[i-1]-s2[j]);
		if(j)f[i][j][1]=f[i][j-1][1]+abs(v2[j].x-v2[j-1].x)*(sv-s1[i]-s2[j-1]);
		f[i][j][0]=min(f[i][j][0],f[i][j][1]+abs(v2[j].x-v1[i].x)*(sv-s1[i]-s2[j]));
		f[i][j][1]=min(f[i][j][1],f[i][j][0]+abs(v1[i].x-v2[j].x)*(sv-s1[i]-s2[j]));
	}
	double res=sy-min(f[v1.size()-1][v2.size()-1][0],f[v1.size()-1][v2.size()-1][1]);
	res/=1000;
	printf("%.3lf\n",res);
	return 0;
}

V.CF559E Gerald and Path

考虑将所有线段按照固定的那一端从小往大排序,并且对线段的端点离散化。

这之后,设 fi,jfi,j 表示当前处理到线段 ii,且所有线段中最右的那根的右端点不右于位置 jj(即可以在 jj 左面或与 jj 重合)时的最优答案。

我们考虑,假设我们放了一根线段 [l,r][l,r]。因为不知道将来会放什么东西把它盖掉一部分,所以我们干脆在放线段 [l,r][l,r] 时,同时也放下线段 [l,l],[l,l+1],[l,l+2],,[l,r1][l,l],[l,l+1],[l,l+2],,[l,r1],这样就不用担心被盖掉等讨论了。

于是我们现在考虑处理第 ii 根线段。设其向左是 [l,p][l,p],向右是 [p,r][p,r]

首先,其有可能在接下来(或者在 ii 之前就已经)被完全覆盖掉。于是一开始就有 fi1,jfi,jfi1,jfi,j

其次,考虑其向右摆。向右摆,就意味着最右位置一定是 rr——若最右位不是 rr,则一定在 ii 之前还有一条向右摆的线段 [p,r][p,r] 满足 rrrr。但是因为我们已经按照 pp 递增排序了,故必有 [p,r][p,r][p,r][p,r],即 [p,r][p,r] 被完全覆盖,我们已经在上面说过了。

则有 fi,rfi1,p+[p,r]fi,rfi1,p+[p,r]——因为我们已经令 fi,jfi,j 表示“不右于”的最优值,所以就不用费尽心思枚举 i1i1 时的最右位置了。同时,也不用担心重叠问题,因为按照我们上述讨论,为了避免重叠,我们直接将线段 [l,r][l,r] 看作了所有 [l,lr][l,lr]

但是这也意味着,我们的线段 [p,r][p,r] 也要被看作是所有的 [p,pr][p,pr]。于是我们枚举 j[p,r]j[p,r],则有 fi,jfi1,p+[p,j]fi,jfi1,p+[p,j]

然后就考虑其向左摆了。向左摆,就意味着最右位置不一定是 pp——因为完全可以存在一条 [p,r][p,r],满足 r>pandp>lr>pandp>l,即两者无交。

首先,最右位是 pp 的就可以跟之前一样类似地转移,不在话下。

然后,对于最右位不是 pp 的,我们枚举一个 j<ij<i,表示最右位是 jj 对应的 [p,r][p,r](明显这里上述 r>pandp>lr>pandp>l 应被满足)。则所有 k(j,i]k(j,i] 都应该向左摆,因为若其向右摆,要么右端点在 rr 左边或与 rr 重合,被完全包含;要么右端点在 rr 右边,则 rr 就不是最右位了。

设所有 k(j,i]k(j,i] 的东西中,最左的那个是 [l,p][l′′,p′′],则整个 [j,i][j,i] 中所有线段,加一块可以变成一个 [l,r][l′′,r] 的大线段,且该线段是最右的。于是此处就可以跟前面一样,枚举一个 k[l,r]k[l′′,r] 进行转移了。

时间复杂度 O(n3)O(n3),因为枚举了 i,j,ki,j,k 三个元素。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[110][310];
pair<int,int>p[110];
vector<int>v;
int L[110],P[110],R[110],res;
#define bs(x) lower_bound(v.begin(),v.end(),x)-v.begin()+1
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&p[i].first,&p[i].second),v.push_back(p[i].first),v.push_back(p[i].first-p[i].second),v.push_back(p[i].first+p[i].second);
	sort(p+1,p+n+1),sort(v.begin(),v.end()),v.resize(m=unique(v.begin(),v.end())-v.begin());
	for(int i=1;i<=n;i++)L[i]=bs(p[i].first-p[i].second),P[i]=bs(p[i].first),R[i]=bs(p[i].first+p[i].second);
	for(int i=1;i<=n;i++){
		memcpy(f[i],f[i-1],sizeof(f[i]));
		for(int j=P[i];j<=R[i];j++)f[i][j]=max(f[i][j],f[i-1][P[i]]+v[j-1]-v[P[i]-1]);
		for(int j=i;j;j--){
			int Rmax=(j==i?P[j]:R[j]);
			if(Rmax<P[i])continue;
			int Lmin=(j==i?L[j]:P[j]);
			for(int k=j+1;k<=i;k++)Lmin=min(Lmin,L[k]);
			for(int k=Lmin;k<=Rmax;k++)f[i][k]=max(f[i][k],f[j-1][Lmin]+v[k-1]-v[Lmin-1]);
		}
		for(int j=1;j<=m;j++)f[i][j]=max(f[i][j],f[i][j-1]);
	}
	for(int i=1;i<=m;i++)res=max(res,f[n][i]);
	printf("%d\n",res);
	return 0;
}

VI.[CERC2017]Kitchen Knobs

首先,一个trivial的想法是,因为每个旋钮如果上面的数字并非全部相同则其必有唯一最优位置,故直接扔掉那些全部相同的旋钮,对于剩余的求出其最优位置。明显此位置是一 0606 的数。

因为是区间同时旋转,所以转成数之后就是区间加同一个数。

一个经典套路是差分。这样就变成了每次同时修改序列中的两个数,一个加 xx,另一个减 xx。当然,还可以只修改一个位置到任意值。

然后我们发现,对于一些和为 77 的倍数的数,我们只需要这些数的总数 11 次“修改两个数”即可处理它们。

然后我们又发现,首先那些本身就是 00 的数可以直接扔掉,再怎么搞都不会修改到它们;这样的话,每次合并所能省下的次数最多只是 50%50%,因此我们一定会合并那些 50%50% 的方案,即合并 1,61,62,52,53,43,4

这样合并完后,我们发现最多只剩下 33 种不同的数,因此一个 n3n3 的DP就可以构思出来了。

然后我们还需要知道所有的和为 77 的组合。并且,这些组合必须是极小组合。于是我们直接 7373 枚举三个数的次数,再 7373 枚举其子集验证其是否是极小组合即可。

最后别忘记算 (7,0,0),(0,7,0),(0,0,7)(7,0,0),(0,7,0),(0,0,7) 这三种组合!!!因为没有考虑到这个我挂了很久。

这样一通算下来,极小组合数可以被视作很小的常数。于是直接 n3n3 DP即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[510],tot[8],f[510][510][510],lim[4],num[4],cmp,res;
string s,t;
int g[510][4],cnt;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>s,t=s;
		bool as=true;
		for(int j=1;j<7;j++)if(s[j]!=s[0]){as=false;break;}
		if(as){i--,n--;continue;}
		for(int j=0;j<7;j++)rotate(s.begin(),s.begin()+1,s.end()),t=max(t,s);
//		cout<<t<<endl;
		for(int j=0;j<7;j++){
			if(s==t){a[i]=j;break;}
			rotate(s.begin(),s.begin()+1,s.end());
		}
//		printf("%d\n",a[i]);
	}
//	for(int i=1;i<=n;i++)printf("%d ",a[i]);puts("")
	n++;
	for(int i=n;i>=2;i--)a[i]=(a[i]-a[i-1]+7)%7;
//	for(int i=1;i<=n;i++)printf("%d ",a[i]);puts("");
	for(int i=1;i<=n;i++)tot[a[i]]++;
	for(int i=1,x;i<=3;i++)x=min(tot[i],tot[7-i]),tot[i]-=x,tot[7-i]-=x,n-=x;
	n-=tot[0];
	for(int i=1;i<7;i++)if(tot[i])lim[cmp]=tot[i],num[cmp]=i,cmp++;
	for(int i=0;i<7;i++)for(int j=0;j<7;j++)for(int k=0;k<7;k++){
		if((i*num[0]+j*num[1]+k*num[2])%7)continue;
		if(!i&&!j&&!k)continue;
		bool ok=true;
		for(int I=0;I<=i;I++)for(int J=0;J<=j;J++)for(int K=0;K<=k;K++){
			if((I*num[0]+J*num[1]+K*num[2])%7)continue;
			if(!I&&!J&&!K||i==I&&j==J&&k==K)continue;
			ok=false;break;
		}
		if(ok)g[cnt][0]=i,g[cnt][1]=j,g[cnt][2]=k,cnt++;
	}
	g[cnt][0]=7,g[cnt][1]=0,g[cnt][2]=0,cnt++;
	g[cnt][0]=0,g[cnt][1]=7,g[cnt][2]=0,cnt++;
	g[cnt][0]=0,g[cnt][1]=0,g[cnt][2]=7,cnt++;
//	puts("");for(int i=0;i<cnt;i++)printf("%d %d %d\n",g[i][0],g[i][1],g[i][2]);
	for(int i=0;i<=lim[0];i++)for(int j=0;j<=lim[1];j++)for(int k=0;k<=lim[2];k++){
		for(int l=0;l<cnt;l++){
			if(i<g[l][0]||j<g[l][1]||k<g[l][2])continue;
			f[i][j][k]=max(f[i][j][k],f[i-g[l][0]][j-g[l][1]][k-g[l][2]]+1);
		}
		res=max(res,f[i][j][k]);
	}
	cout<<n-res<<endl;
	return 0;
}

II.性质分析类 DP

针对某些特殊的状态,我们要加以分析才能设计成功。

I.[HAOI2018]奇怪的背包

神题。

  1. 对于某个大小为vv的物品,它所能表示出的位置的集合等于gcd(v,P)gcd(v,P)所能表示的集合。

  2. 对于某些大小为v1,,vkv1,,vk的物品,位置集合为gcd{v1,,vk,P}gcd{v1,,vk,P}

因此考虑DP。

我们找出所有PP的约数,存入vector。(这个个数的级别设为LL,则LL最大只到768768。)设PP的第ii个约数为pipi

则对于所有的vivi,我们找出gcd(P,vi)gcd(P,vi)。设新的vi=gcd(P,vi)vi=gcd(P,vi)

对于每个pipi,统计它在v1,,vnv1,,vn中出现了多少次,设为sisi

我们设f[i][j]f[i][j]表示:在PPii个约数中,选择一些数,使得他们的gcdgcd等于PP的第jj个约数的方案数。

则有

f[i][j]=f[i1][j]+({1(i=j)0(ij)+gcd(pk,pi)=pjf[i1][k])(2si1)f[i][j]=f[i1][j]+({1(i=j)0(ij)+gcd(pk,pi)=pjf[i1][k])(2si1)

释义:

首先,答案是可以从前一位继承来的。

然后,因为对于每个i,选任何数量的vk=pik都是等价的,因此共有2si1中选法;

i=j时,可以之前一个数也不选,就选i一个数,因此要加上1

然后,因为gcd具有结合律和交换律,所有gcd(pk,pi)=pj的状态也是可继承的。

f[n][j]的状态是最终状态。

对于每个wi,答案为vj|wif[n][j]。这个可以通过一个L2的预处理求出g[i]=vj|vif[n][j]算出。

复杂度O(P+L2logL+q)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,p,s[1001000],f[2][1001000],g[1001000],two[1001000];
vector<int>v; 
int main(){
	scanf("%d%d%d",&n,&m,&p);
	two[0]=1;
	for(int i=1;i<=n;i++)two[i]=(two[i-1]<<1)%mod;
	for(int i=0;i<=n;i++)two[i]=(two[i]-1+mod)%mod;
//	for(int i=0;i<=n;i++)printf("%d ",two[i]);puts("");
	for(int i=1;i*i<=p;i++){
		if(p%i)continue;
		v.push_back(i);
		if(i*i!=p)v.push_back(p/i);
	}
	sort(v.begin(),v.end());
//	for(auto i:v)printf("%d ",i);puts("");
	for(int i=1,x;i<=n;i++)scanf("%d",&x),x=__gcd(x,p),s[lower_bound(v.begin(),v.end(),x)-v.begin()]++;
//	for(int i=0;i<v.size();i++)printf("%d ",s[i]);puts("");puts("");
	for(int i=0;i<v.size();i++){
		for(int j=0;j<=i;j++)f[i&1][j]=0;
		f[i&1][i]=1;
		for(int j=0;j<i;j++){
			int gcd=__gcd(v[i],v[j]);
			gcd=lower_bound(v.begin(),v.end(),gcd)-v.begin();
			(f[i&1][gcd]+=f[!(i&1)][j])%=mod;
		}
		for(int j=0;j<=i;j++)f[i&1][j]=(1ll*f[i&1][j]*two[s[i]]+f[!(i&1)][j])%mod;
//		for(int j=0;j<=i;j++)printf("%d ",f[i&1][j]);puts("");
	}
	for(int i=0;i<v.size();i++)for(int j=0;j<=i;j++)if(!(v[i]%v[j]))(g[i]+=f[n&1][j])%=mod;
	for(int i=1,w;i<=m;i++)scanf("%d",&w),w=__gcd(w,p),printf("%d\n",g[lower_bound(v.begin(),v.end(),w)-v.begin()]);
	return 0;
} 

II.[IOI2005]Riv 河流

新转移方式get~~~

我必须吐槽一下现在赞最多的那篇题解,虽然思路巧妙,但是明显没有“物尽其用”,对于各DP数组的真实含义也没有把握清楚。

一个naive的想法就是:设f[i][j]表示:在i的子树中,修了j个场子,的最小费用。

但是这样不是很好转移——子树传上来的信息不能直接合并,因为我们必须知道场子到底修哪了才能准确得出答案。

而我们又不可能在状态里面维护这么多场子——状压不了。

等等,我们为什么要从根记录子树,为什么不是从子树记录根?

我们设f[x][j][k]表示:

x为根的子树中,修了k个堡。并且,强制在第j个点修个堡(ji的祖先)。

这样,合并子树时就可以直接背包了——因为每个节点流到的堡确定了,代价自然就可以提前算出。

即:

f[x][j][k]=max{f[x][j][l]+f[y][j][kl]},y is a son of x

每次将x的状态同y合并。

但这样就会出现一些问题——我们说要在j修个堡,但是这只是空头支票,没有算到k里面,当访问到j时,这个债是要还的!

因此对于f[x][x][k],我们应该让k全体右移一位,即f[x][x][k]=f[x][x][k1],且f[x][x][0]=(欠的一个堡还不回来,只能破产)。

还有,我们要计算x位置新产生的代价。这个代价要么x位置额外再修一个堡,要么就是到j的距离。因此我们有

f[x][j][k]=min(f[x][j][k]+valx(disxdisk),f[x][x][k])

则答案为f[0][0][K+1]0号点有个免费的堡)。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,head[110],cnt,f[110][110][60],g[60],val[110],dis[110],anc[110],tp;
struct node{
	int to,next,val;
}edge[210];
void ae(int u,int v,int w){
	edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
}
void dfs(int x){
	anc[++tp]=x;
	for(int i=head[x],y;i!=-1;i=edge[i].next){
		y=edge[i].to,dis[y]=dis[x]+edge[i].val,dfs(y);
		for(int j=1;j<=tp;j++){
			for(int k=0;k<=m;k++)g[k]=0x3f3f3f3f;
			for(int k=0;k<=m;k++)for(int l=0;l<=k;l++)g[k]=min(g[k],f[x][anc[j]][k-l]+f[y][anc[j]][l]);
			for(int k=0;k<=m;k++)f[x][anc[j]][k]=g[k];
		}
	}
	for(int j=m;j;j--)f[x][x][j]=f[x][x][j-1];
	f[x][x][0]=0x3f3f3f3f;
	for(int j=1;j<tp;j++)for(int k=0;k<=m;k++)f[x][anc[j]][k]+=val[x]*(dis[x]-dis[anc[j]]),f[x][anc[j]][k]=min(f[x][anc[j]][k],f[x][x][k]);
	tp--;
}
int main(){
	scanf("%d%d",&n,&m),m++,memset(head,-1,sizeof(head));
	for(int i=1,x,y;i<=n;i++)scanf("%d%d%d",&val[i],&x,&y),ae(x,i,y);
	dfs(0);
//	for(int i=1;i<=n;i++)printf("%d ",dis[i]*val[i]);puts("");
	printf("%d\n",f[0][0][m]);
	return 0;
}

III.CF1088E Ehab and a component choosing problem

思路1.n2DP。

考虑设f[i][j][0/1]表示:

节点i,子树分了j个集合,节点i是/否在某个集合内的最大值。

但是这样是没有前途的——你再怎么优化也优化不了,还是只能从题目本身的性质入手。

思路2.分析性质,O(n)解决。

发现,答案最大也不会超过最大的那个集合的和。

我们考虑把每个集合看成一个数。那么,题目就让我们从一堆数中选一些数,使得它们的平均值最大。只选最大的那一个数,则答案就是最大的那一个数;但是最大的数可能不止一个,因此要找到所有值最大且互不相交的集合的数量。

找到最大的那个集合,可以直接O(n)DP出来。设fx表示以x为根的子树中,包含x的所有集合中最大的那个,则有

fx=ysonxmax(fy,0)

这样最大的那个集合就是fx的最大值。

至于互不重叠的限制吗……再DP一遍,当一个fx达到最大时,计数器++,并将fx清零。

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,val[300100],f[300100],mx=0x8080808080808080,res,head[300100],cnt;
bool vis[300100];
struct node{
	int to,next;
}edge[600100];
void ae(int u,int v){
	edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
	edge[cnt].next=head[v],edge[cnt].to=u,head[v]=cnt++;
}
void dfs1(int x,int fa){
	f[x]=val[x];
	for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)dfs1(edge[i].to,x),f[x]+=max(0ll,f[edge[i].to]);
}
void dfs2(int x,int fa){
	f[x]=val[x];
	for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)dfs2(edge[i].to,x),f[x]+=max(0ll,f[edge[i].to]);
	if(f[x]==mx)res++,f[x]=0x8080808080808080;
}
signed main(){
	scanf("%lld",&n),memset(head,-1,sizeof(head));
	for(int i=1;i<=n;i++)scanf("%lld",&val[i]);
	for(int i=1,x,y;i<n;i++)scanf("%lld%lld",&x,&y),ae(x,y);
	dfs1(1,0);
	for(int i=1;i<=n;i++)mx=max(mx,f[i]);
	dfs2(1,0);
	printf("%lld %lld\n",1ll*res*mx,res);
	return 0;
}

IV.CF908D New Year and Arbitrary Arrangement

思路:

期望题果然还是恶心呀……

我们设f[i][j]表示当串中有iajab时的方案数。为了方便,设A=PaPa+Pb,B=PbPa+Pb

显然,可以这样转移:

f[i][j]=f[i+1][j]A+f[i][i+j]B

因为,如果串后面加上一个a,概率是A,并且加完后唯一的影响就是i+1;如果加入一个b,概率是B,加完后前面每一个a都会与这个b形成一对ab

那么边界条件呢?

显然,当i+jk时,只要再往后面加入一个b,过程就停止了。

则这个的期望长度应该是:

Ba=0(i+j+a)Aa

其中,枚举的这个a是在终于搞出一个b前,所刷出的a的数量。

为了方便,我们设i+j=c,并用i替换a。即:

Bi=0(c+i)Ai

因为A+B=1,我们可以用(1A)B

即:

(1A)i=0(c+i)Ai

拆开括号得

i=0(c+i)Aii=0(c+i)Ai+1

一上来直接有些不直观,我们用n替换掉。

ni=0(c+i)Aini=0(c+i)Ai+1

在第二个式子里面用i+1代掉i

ni=0(c+i)Ain+1i=1(c+i1)Ai

将第一个Σi=0的情况和第二个Σi=n+1的情况分别提出

c+ni=1(c+i)Aini=1(c+i1)Ai(c+n)An+1

合并两个Σ

c+ni=1Ai(c+n)An+1

套等比数列求和公式(注意要先提出一个A使首项为1

c+A1An1A(c+n)An+1

注意到1A=B

c+A1AnB(c+n)An+1

现在,考虑n的情况。即:

limnc+A1AnB(c+n)An+1

注意到0<A<1,因此limnAn=0

带入发现

c+A10B(c+n)0

处理一下

c+AB

注意到我们一开始的定义了吗?

A=PaPa+Pb,B=PbPa+Pb

以及c=i+j

代入得

i+j+PaPb

也就是说,边界条件就是f[i][j]=i+j+PaPb(i+jk)!!!

再搬出我们一开始的转移式

f[i][j]=f[i+1][j]A+f[i][i+j]B

完事。

哦,另外,还要思考一下答案到底是f[0][0]还是f[1][0]

因为一开始的那些b,无论来多少个都是没用的,因此不如直接从f[1][0]开始。(事实上,你如果把转移式代回去或者打个表的话,你会发现就有f[0][0]=f[1][0]

复杂度O(k2+logmod)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a,b,A,B,f[1010][1010],c;
const int mod=1e9+7;
int ksm(int x,int y){
    int z=1;
    for(;y;x=(1ll*x*x)%mod,y>>=1)if(y&1)z=(1ll*z*x)%mod;
    return z;
}
int dfs(int x,int y){
    if(x+y>=n)return x+y+c;
    if(f[x][y]!=-1)return f[x][y];
    int &res=f[x][y];res=0;
    (res+=1ll*dfs(x+1,y)*A%mod)%=mod;
    (res+=1ll*dfs(x,x+y)*B%mod)%=mod;
    return res;
}
int main(){
    scanf("%d%d%d",&n,&a,&b),A=1ll*a*ksm(a+b,mod-2)%mod,B=1ll*b*ksm(a+b,mod-2)%mod,c=1ll*a*ksm(b,mod-2)%mod,memset(f,-1,sizeof(f));
    printf("%d\n",dfs(1,0));
    return 0;
}

V.[USACO17JAN]Subsequence Reversal P

思路:

发现,翻转一个子序列,就意味着两两互换子序列里面的东西。

于是我们就可以设f[l][r][L][R]表示:max[1,l)=L,min(r,n]=R时的最长长度。

则边界为:L>R时,f=;否则,如果l>rf=0

然后开始转移。

  1. 不选

f[l+1][r][L][R]f[l][r1][L][R]

  1. 选一个

alL时,f[l+1][r][al][R]+1

arR时,f[l][r1][L][ar]+1

  1. 翻转(必须有l<r

arL时,f[l+1][r1][ar][R]+1

alR时,f[l+1][r1][L][al]+1

arLalR时,f[l+1][r1][ar][al]+2

最终答案为f[1][n][0][],其中=50足矣。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,f[60][60][60][60],a[60];
int solve(int l,int r,int L,int R){
	if(L>R)return 0x80808080;
	if(l>r)return 0;
	if(f[l][r][L][R]!=-1)return f[l][r][L][R];
	int &res=f[l][r][L][R];res=0;
	res=max(res,solve(l+1,r,L,R));
	res=max(res,solve(l,r-1,L,R));
	if(a[l]>=L)res=max(res,solve(l+1,r,a[l],R)+1);
	if(a[r]>=L&&l!=r)res=max(res,solve(l+1,r-1,a[r],R)+1);
	if(a[r]<=R)res=max(res,solve(l,r-1,L,a[r])+1);
	if(a[l]<=R&&l!=r)res=max(res,solve(l+1,r-1,L,a[l])+1);
	if(a[l]<=R&&a[r]>=L&&l!=r)res=max(res,solve(l+1,r-1,a[r],a[l])+2);
//	printf("(%d,%d):(%d,%d):%d\n",l,r,L,R,res);
	return res;
}
int main(){
	scanf("%d",&n),memset(f,-1,sizeof(f));
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	printf("%d\n",solve(1,n,0,50));
	return 0;
}

VI.[USACO18JAN]Stamp Painting G

思路:

发现任何具有一段长度大于等于K的相同颜色区间的串都是合法的(这个区间被看作最后一次染色的目标)。

因此反向思考,我们求出所有不具有长度大于等于k的相同颜色区间的串数量,然后用总数量(MN)减一下即可。

我们设f[i]表示前i位的方案数。

则有

f[i]={Mi(1i<K)(i1j=iK+1f[j])(M1)

复杂度O(NK)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,p,f[1001000],res=1;
int main(){
	scanf("%d%d%d",&n,&m,&p);
	f[0]=1;
	for(int i=1;i<p;i++,res=(1ll*res*m)%mod)f[i]=(1ll*f[i-1]*m)%mod;
	for(int i=p;i<=n;i++,res=(1ll*res*m)%mod){
		for(int j=i-p+1;j<i;j++)(f[i]+=f[j])%=mod;
		f[i]=1ll*f[i]*(m-1)%mod;
	}
//	for(int i=1;i<=n;i++)printf("%d ",f[i]);
	printf("%d\n",(mod+res-f[n])%mod);
	return 0;
}

然后套上前缀和,复杂度O(N)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,p,f[1001000],res=1,s[1001000];
int main(){
	scanf("%d%d%d",&n,&m,&p);
	f[0]=1;
	for(int i=1;i<p;i++,res=(1ll*res*m)%mod)f[i]=(1ll*f[i-1]*m)%mod,s[i]=(s[i-1]+f[i])%mod;
	for(int i=p;i<=n;i++,res=(1ll*res*m)%mod)f[i]=1ll*(s[i-1]-s[i-p]+mod)%mod*(m-1)%mod,s[i]=(s[i-1]+f[i])%mod;
	printf("%d\n",(mod+res-f[n])%mod);
	return 0;
}

VII.[USACO19DEC]Greedy Pie Eaters P

考场上写了个暴力贪心(因为看到题面中的 greedy ……)然后光荣爆炸……

因为n300,考虑区间DP。

f[i][j]表示有且只有区间[i,j]里的π被吃完后的最大收益。

则我们可以得到如下转移:

f[i][j]=jmaxk=if[i][k1]+???+f[k+1][j]

含义为:我们特地留下第kπ不吃,剩下全吃掉,然后选择能吃到第kπ的最大的那头牛。

而这个???,就是那头牛的体重。

我们思考这头牛必须具有什么特征:

首先,它所吃掉的π,必定是[i,j]的子区间;

其次,这个区间里必须包含第kπ

因此,我们设g[i][j][k]表示这样的牛的最大体重。

然后g也可以通过区间DP算出。

复杂度O(n3)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[510][510],g[510][510][510];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,x,y,z;i<=m;i++){
		scanf("%d%d%d",&z,&x,&y);
		for(int j=x;j<=y;j++)g[x][y][j]=max(g[x][y][j],z);
	}
	for(int k=1;k<=n;k++)for(int i=k;i>=1;i--)for(int j=k;j<=n;j++)g[i][j][k]=max(g[i][j][k],max(g[i+1][j][k],g[i][j-1][k]));
	for(int l=1;l<=n;l++)for(int i=1,j=i+l-1;j<=n;i++,j++)for(int k=i;k<=j;k++)f[i][j]=max(f[i][j],f[i][k-1]+g[i][j][k]+f[k+1][j]);
	printf("%d\n",f[1][n]);
	return 0;
}

VIII.[USACO20FEB]Help Yourself G

思路:

考虑将线段按照左端点排序。

f[i]表示前i个线段的复杂度之和。

f[i]=2f[i1]+2sum[li1]。其中sumi是右端点i的线段数目,lii线段的左端点。

思考:

再往前的复杂度之和,无论第i根线段选与不选,都是无法改变的。因此要2

当之前的线段与线段i交集为空时,联通块新增一块。这部分共有2sum[li1]种选法。

复杂度O(nlogn)(排序是瓶颈,如果换成桶排就是O(n)的)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,sum[200100],bin[100100],f[100100];
pair<int,int>p[100100];
int main(){
	scanf("%d",&n),bin[0]=1;
	for(int i=1;i<=n;i++)scanf("%d%d",&p[i].first,&p[i].second),sum[p[i].second]++,bin[i]=(bin[i-1]<<1)%mod;
	for(int i=1;i<=2*n;i++)sum[i]+=sum[i-1];
	sort(p+1,p+n+1);
	for(int i=1;i<=n;i++)f[i]=(2ll*f[i-1]+bin[sum[p[i].first-1]])%mod;
	printf("%d\n",f[n]);
	return 0;
}

IX.CF261D Maxim and Increasing Subsequence

首先,我们可以发现,当这个重复次数很大的时候,答案就等于序列中出现的不同权值个数。实际上,这个“很大”就可以被当作“大于等于不同权值个数”。

不同权值个数实际上是min(n,m)级别的,其中n是序列长度,m是序列最大值。因此直接特判掉即可。

我们考虑暴力DP。设fi,j表明现在跑到序列中的第i个位置,且所有最后一个数小于等于j的LIS的长度的最大值。假如我们直接暴力扫过DP数组更新的话,最多最多更新min(n,m)2次,即最终把DP数组中所有数全都更新到最大值。而又有n×m2×107,所以我们最终会发现复杂度最大只有2×107。时限6秒,轻松跑过。

代码:

#include<bits/stdc++.h>
using namespace std;
int T,n,lim,m,a[100100],f[100100];
vector<int>v;
int main(){
	scanf("%d%d%d%d",&T,&n,&lim,&m);
	while(T--){
		v.clear();
		for(int i=0;i<n;i++)scanf("%d",&a[i]),v.push_back(a[i]);
		sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
		if(v.size()<=m){printf("%d\n",v.size());continue;}
		for(int i=0;i<n;i++)a[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1;
		for(int i=1;i<=v.size();i++)f[i]=0;
		for(int i=0;i<n*m;i++){
			int now=f[a[i%n]-1]+1;
			for(int j=a[i%n];j<=v.size();j++)if(f[j]<now)f[j]=now;else break;
			if(f[v.size()]==v.size())break;
		}
		printf("%d\n",f[v.size()]);
	}
	return 0;
}

X.CF979E Kuro and Topological Parity

我们考虑在一张染色完成的图里,我们连上了一条边,会有何影响?

  1. 在同色节点间连边——明显不会有任何影响
  2. 在异色节点间连边,但是出发点是个偶点(即有偶数条路径以其为终点的节点)——终点的路径数增加了,但增加的是偶数,故也无影响。
  3. 在异色节点间连边,但是出发点是个奇点——终点的路径数的奇偶态变化,有影响。

故我们只需要考虑状况三即可。

于是我们就可以构造出如下的DP:

f[i,j,k,l]表示当前DP到了位置i,总路径数是j0/1),且无/有奇黑点,无/有奇白点。

下面以位置i+1填入白色为例:

  1. 存在至少一个奇黑点(即k=1),则对于任意一组其它i1个节点的连边方式,总有一种方式使得总数为奇,一种方式使得总数为偶(受此奇黑点的控制)。于是就有 f[i,j,k,l]×2i1f[i+1,j,k,l]f[i,j,k,l]×2i1f[i+1,¬j,k,true]
  2. 不存在奇黑点(即k=0),则无论怎么连,i+1的奇偶性都不会变化,始终为奇态(被看作是以它自己为起点的路径的终点)。故有f[i,j,k,l]×2if[i+1,¬j,k,true]

填入黑色则同理。

代码

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,p,a[100],f[100][2][2][2],bin[100],res;
//f[i][j][k][l]:the number of situations where there're odd/even roads which ends in i, there has(not) an odd black, has(not) an odd white
int main(){
	scanf("%d%d",&n,&p),bin[0]=1;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),bin[i]=(bin[i-1]<<1)%mod;
	f[0][0][0][0]=1;
	for(int i=0;i<n;i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)for(int l=0;l<2;l++){
		if(!f[i][j][k][l])continue;
		int tmp=f[i][j][k][l];
		if(a[i+1]!=0){//can be white
			if(k)(f[i+1][j][k][l]+=1ll*tmp*bin[i-1]%mod)%=mod,(f[i+1][j^1][k][true]+=1ll*tmp*bin[i-1]%mod)%=mod;
			else (f[i+1][j^1][k][true]+=1ll*tmp*bin[i]%mod)%=mod;
		}
		if(a[i+1]!=1){//can be black
			if(l)(f[i+1][j][k][l]+=1ll*tmp*bin[i-1]%mod)%=mod,(f[i+1][j^1][true][l]+=1ll*tmp*bin[i-1]%mod)%=mod;
			else (f[i+1][j^1][true][l]+=1ll*tmp*bin[i]%mod)%=mod;
		}
	}
	for(int k=0;k<2;k++)for(int l=0;l<2;l++)(res+=f[n][p][k][l])%=mod;
	printf("%d\n",res);
	return 0;
}

XI.[POI2013]BAJ-Bytecomputer

一道大力猜结论的题。

首先先说猜想:最终序列中所有数都是1,0,1,且不存在先改后面,后改前面的状态。

有了这个猜想,就可以DP了。我们设fi,j表示要使位置i出现数j,且前i个位置单调不降的最小费用。则我们枚举往ai+1上加多少个j(明显只能加0/1/2个),判断往ai+1上加上这么多j后是否仍满足单调不降,如果可以那就直接转移没问题了。

下面来讲证明。全是1,0,1很好证,因为原本所有数都在此值域内,你要加出这个值域肯定要耗费更多代价。

不存在先改后面,后改前面的状态也很好证——首先,我们观察到执行a[i]+=a[i-1],肯定有一种方案使得要么它在ai1符合要求之前执行,要么它在ai1符合要求之后执行。而两次执行,都是+1/0/1,假如两次相同,那肯定可以看作一端执行两遍;有一个是0,不如不执行;则只剩下一次+1,一次1的状况,但这样等价于没执行,所以也可以忽略。

则上述解法正确。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[1000100],f[1001000][3],res=0x3f3f3f3f;
int main(){
	scanf("%d",&n),memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	f[1][a[1]+1]=0;
	for(int i=1;i<n;i++)for(int j=-1;j<=1;j++)for(int k=0;k<=2;k++)if(a[i+1]+k*j>=j&&a[i+1]+k*j<=1)f[i+1][a[i+1]+k*j+1]=min(f[i+1][a[i+1]+k*j+1],f[i][j+1]+k);
	for(int i=-1;i<=1;i++)res=min(res,f[n][i+1]);
	if(res==0x3f3f3f3f)puts("BRAK");else printf("%d\n",res);
	return 0;
}

XII.[ARC067D] Yakiniku Restaurants

明显在最优方案中,行走方式一定是从一条线段的一端走到另一端,不回头。

于是设 f[i,j] 表示从 i 走到 j 的最优代价。明显,该代价对于不同的券相互独立。故我们依次考虑每一张券。

我们发现,假设有一张位置 k 的券,则所有 k[l,r][l,r] 都是可以享受到它的。于是,我们建出笛卡尔树来,就可以把它用差分轻松解决了(假设笛卡尔树上有一个节点 x,它是区间 [l,r] 中的最大值,则所有区间 [l,r] 中穿过它的区间都会增加 ax,但是它的两个子区间 [l,x1][x+1,r] 却享受不到,故在该处再减少 ax,即可实现差分地更新。

则时间复杂度 O(nm+n2)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[210][5010],stk[5010],tp,lson[5010],rson[5010];
ll s[5010],f[5010][5010],res;
void solve(int id,int x,int l,int r,int las){
	f[l][r]+=a[id][x]-las;
	if(l==r)return;
	if(lson[x])solve(id,lson[x],l,x-1,a[id][x]);
	if(rson[x])solve(id,rson[x],x+1,r,a[id][x]);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=2;i<=n;i++)scanf("%lld",&s[i]),s[i]+=s[i-1];
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[j][i]);
	for(int i=1;i<=m;i++){
		tp=0;
		for(int j=1;j<=n;j++){
			lson[j]=rson[j]=0;
			while(tp&&a[i][stk[tp]]<=a[i][j])lson[j]=stk[tp--];
			if(stk[tp])rson[stk[tp]]=j;
			stk[++tp]=j;
		}
		solve(i,stk[1],1,n,0);
	}
	for(int i=1;i<=n;i++)for(int j=n;j>=i;j--)f[i][j]+=f[i-1][j]+f[i][j+1]-f[i-1][j+1];
	for(int i=1;i<=n;i++)for(int j=i;j<=n;j++)res=max(res,f[i][j]-(s[j]-s[i]));
	printf("%lld\n",res);
	return 0;
}

XIII.CF868E Policeman and a Tree

DP是很容易想的。但是如何设计状态呢?

一开始我自己假设了一个结论:在警察出发前,所有罪犯会排成此时的最优方案,然后不动;然后在警察抓到一个罪犯后,所有罪犯会再度排成最优方案,之后就一直不动了。但是如果这样做的话 50 的数据范围就像在开玩笑一样,因此我不确定这个结论是否正确。

但是这个 50 的数据范围就意味着我们不需要顾忌那么多,可以放心在状态里设一大坨东西。于是我们设 fi,j,k 表示当前警察在边 i(看作有向边)半道上,剩 j 个罪犯,k 个在该边的终点侧。转移有两类:一种是该边的终点(设为 x)是叶子节点,警察抓住了叶子上所有小偷,所以 fi,j,kfi,jk,jk+w,其中 ii 的反边,而 w 是边权。另一种是 x 并非叶子节点,此时罪犯可以先分布成最优状态,等警察走到其中某个叶子上。警察想要最优,于是罪犯就必须让警察的最优选择是所有情形中最劣的。于是就有 fi,j,kmaxc is an assignment of k into x's sons{minysonxfxy,j,cy+w}

暴力转移是 O(n5) 的,因为要拼接两半的背包,但已经足以通过。但是,通过观察性质,可以进一步优化。该性质是,随着状态由 fi,j,k 变为 fi,j,k+1,最优的 c 的分配只会是在上一个 c 的分配的基础上使某个位置加一得到。更具体地,该加一的位置一定是所有位置中加一后结果最大的那个。证明如下:

因为是最小值最大的形式,就考虑二分。二分后,我们就只能保留所有 某一值的DP状态。而又显然 fi,j,k 随着 k 的递增是不增的(追的人越多,结束得越早),故保留的只能是每个 i,j 的前缀。每次取最大的转移是最有可能拉高DP水平的。

于是直接用大根堆维护即可。时间复杂度 O(n3logn)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,S,f[120][60][60],head[60],deg[60],cnt,sz[60],res=0x3f3f3f3f;//policeman on edge i;j terrorists are remaining;k terrorists on his direction
struct node{int to,next,val;}edge[120];
void ae(int u,int v,int w){
	edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
	edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
void DP(int id,int u);
int F(int x,int y,int z){if(!y)return 0;if(f[x][y][z]==-1)DP(x,y);return f[x][y][z];}
struct dat{
	int x,y,z;
	dat(int X,int Y,int Z){x=X,y=Y,z=Z;}
	int val()const{return z<y?F(x,y,z+1):0;}
	friend bool operator<(const dat&u,const dat&v){return u.val()<v.val();}
};
void DP(int id,int u){
	f[id][u][0]=0x3f3f3f3f;
	if(deg[edge[id].to]==1){for(int i=1;i<=u;i++)f[id][u][i]=F(id^1,u-i,u-i)+edge[id].val;return;}
	priority_queue<dat>q;
	for(int i=head[edge[id].to];i!=-1;i=edge[i].next)if((i^id)!=1)q.emplace(i,u,0);
	for(int i=1;i<=u;i++){
		dat x=q.top();q.pop();
		f[id][u][i]=min(f[id][u][i-1],x.val()+edge[id].val);
		x.z++,q.push(x);
	}
}
void dfs(int x,int fa){for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)dfs(edge[i].to,x),sz[x]+=sz[edge[i].to];}
int main(){
	scanf("%d",&n),memset(head,-1,sizeof(head)),memset(f,-1,sizeof(f));
	for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z),deg[x]++,deg[y]++;
	scanf("%d%d",&S,&m);
	for(int i=1,x;i<=m;i++)scanf("%d",&x),sz[x]++;
	dfs(S,0);
	for(int i=head[S];i!=-1;i=edge[i].next)res=min(res,F(i,m,sz[edge[i].to]));
	printf("%d\n",res);
	return 0;
}

XIV.CF568E Longest Increasing Subsequence

LIS问题有两种主流 O(nlogn) 解法——最常见的树状数组法,以及不那么常见的二分法。然后考虑本题,发现一个trivial的思路就是求出LIS后倒序复原出数组。

进一步思考后发现,因为本题是LIS(Longest Increasing Subsequence)而非LNDS(Longest Non-Decresing Subsequence),所以任意值在LIS中只出现一次,所以我们完全可以不用纠结“一个数只能补一个空缺的位置”这个限制,直接忽略它,求出LIS时所有在LIS中的空缺的填补方法,然后再用尚未被填补的数来补尚未被填补的位置即可。

然后就轮到我们考虑使用哪种LIS了。树状数组法似乎不好扩展(除非把每个空位填哪个数都给枚举一遍扔进BIT里面,但是这样很明显一共要扔 103×105=108 次,单次O(1) 还勉强可以,O(logn) 你在想桃子),因此我们不妨考虑一下二分法。

于是我们发现二分法是可以的,因为其状态 fi 表示长度为 i 的LIS的末尾数最小可能是多少,在非空位处直接二分,在空位处原本也要枚举填哪个数然后二分,但是很明显可以用 two-pointers 轻松 O(n) 处理。于是复杂度便变为 nlogn+k(n+m)

于是我们现在便可以求出LIS长度,考虑复原序列。

我们发现肯定要求出一个 gi 表示以位置 i 结尾的LIS的最长长度。为了实现快速倒推,还要对非空位处的 g 求出其前驱位置,记作 lasi。而 las 又可以通过在DP的过程中对每个 f 维护该最小的末尾数的位置 pos 来求出。

于是我们考虑倒推。对于一个非空缺的位置,我们显然可以通过 las 直接倒推;对于一个空缺的位置,我们考虑优先倒推到非空的位置(这个可以通过枚举得到);否则,即其无法倒推到非空位置,也即其必倒推到空位,而此时我们肯定想贪心地倒推到最后一个空位,那就倒吧。

时间复杂度如上,即为 nlogn+k(n+m)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[100100],m,b[100100],lim,f[100100],pos[100100],g[100100],las[100100];
multiset<int>s;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;i++)scanf("%d",&b[i]),s.insert(b[i]);
	sort(b+1,b+m+1);
	memset(f,0x3f,sizeof(f)),f[0]=0;
	for(int i=1;i<=n;i++){
		if(a[i]!=-1){
			int p=lower_bound(f+1,f+n+1,a[i])-f;
			if(f[p]>a[i])f[p]=a[i],pos[p]=i;
			g[i]=p,las[i]=pos[p-1];
		}else{
			for(int j=m,k=i;j;j--){
				while(k&&f[k-1]>=b[j])k--;
				if(k&&f[k]>b[j])f[k]=b[j],pos[k]=i;
				g[i]=max(g[i],k);
			}
		}
	}
//	for(int i=1;i<=n;i++)printf("(%d:%d,%d)",i,a[i],g[i]);puts("");
	int i=n;
	while(f[i]==0x3f3f3f3f)i--;
	for(int j=pos[i],nex=0x3f3f3f3f;i;i--){
		if(a[j]!=-1)nex=a[j],j=las[j];
		else{
			s.erase(s.find(nex=a[j]=*(lower_bound(b+1,b+m+1,nex)-1)));
			bool fd=false;
			for(int k=j-1;k;k--)if(a[k]!=-1&&g[k]==i-1&&a[k]<a[j]){j=k,fd=true;break;}
			if(fd)continue;
			while(--j)if(a[j]==-1)break;
		}
	}
	for(int i=1;i<=n;i++)if(a[i]==-1)a[i]=*s.begin(),s.erase(s.begin());
	for(int i=1;i<=n;i++)printf("%d ",a[i]);puts("");
	return 0;
}

XV.[TopCoder-10748]StringDecryption

神题。

一次展开怎么办?

fi 表示前 i 位的展开方案,然后枚举上一个断点 j。则,该分段方案合法,当且仅当:

  • aj+1 非零。
  • ji2
  • ajai

总结一下我们需要知道的有关 j 的信息,发现只有 aj 一条。

现在考虑二次展开。一个显然的想法是 fi,j 表示一次展开了前 i 位,并且上次二次展开使用了数字 j。但是这样就会发现不知道能不能填前导零,所以还要额外记录一个 k=0/1 来表示上一个一次展开最后没有/有留下一段剩余的数字,换句话说就是不能/能出现 0

现在仍然是枚举上一个断点记作 l。则首先先判断 (l,i) 这段一次展开是否合法就不谈了。然后关键是二次展开。有如下几种可能:

  • 二次展开的终点与一次展开重合。这时要求二次展开的长度所对应的串并非空串,即 l<i2ai1>1k=1 三者至少满足一个,不然你就会发现展开的是 x 这样的单个数字。同时,还要满足二次展开的长度非零,即要么 k=1,要么 ai0,不然你就会悲伤地发现二次展开展开了一个形如 00...000 的串。之后,就可以转移到 fi,ai,0
  • 二次展开的终点落在了一次展开的串内,但是并没有与一次展开的终点重合,换句话说就是一次展开的串还剩下半截匀给了下个二次展开。这时仍然要检查二次展开次数非零,即 l<i2ai1>1k=1 三者居其一。同时,因为剩下半截要作为下次展开的开头,所以要保证 ai0。全部满足后,就总共有 ¯al+1al+2ai+k2 种将一次展开的串分为两半,其中上划线意为将数码连在一块形成的十进制数。
  • 二次展开的终点完美避开了一次展开的串。这时直接转移到 fi,j,1 即可。但还是需要保证 k=1ai0,不然就会同情形一一样出问题。

这样,时间复杂度 O(kn2),其中 k 是进制单位,此处取 10

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+9;
class StringDecryption{
private:
	int n,a[510],f[510][11][2],pov[510];
public:
	int decrypt(vector<string>code){
		n=0;for(auto i:code)for(auto j:i)a[++n]=j-'0';
		pov[0]=1;for(int i=1;i<=n;i++)pov[i]=(10ll*pov[i-1])%mod;
		memset(f,0,sizeof(f)),f[0][10][0]=1;
		for(int i=2;i<=n;i++){
			for(int l=i-2,val=0;l>=0;l--){
				(val+=1ll*pov[i-2-l]*a[l+1]%mod)%=mod;
				for(int j=0;j<=10;j++)for(int k=0;k<2;k++){
					if(!a[l+1]||a[l]==a[i])continue;
					if(j!=a[i]&&(l!=i-2||a[i-1]!=1||k)&&(k||a[i]))(f[i][a[i]][0]+=f[l][j][k])%=mod;
					if(j!=a[i]&&(l!=i-2||a[i-1]!=1||k)&&a[i])(f[i][a[i]][1]+=1ll*f[l][j][k]*(val+k+mod-2)%mod)%=mod;
					if(k||a[i])(f[i][j][1]+=f[l][j][k])%=mod;
				}
			}
//			for(int j=0;j<=10;j++)for(int k=0;k<2;k++)printf("[%d,%d,%d]:%d\n",i,j,k,f[i][j][k]);
		}
		return f[n][a[n]][0];
	}
}my;

XVI.CF1368H1 Breadboard Capacity (easy version)

Definition 1.我们定义“格点”为板子中央的点,“边缘点”为格子边缘的点、

首先,可以发现,如果图上每个格点向上下左右连边权为 1 的边,并且源点连到蓝边缘点、红边缘点连到汇点,那么答案为最大流。

最大流不好,换成最小割。于是我们要找一条切分红区域与蓝区域的分界线,且分界线长度最小。

Observation 1.分界线不可能成环,也即不可能有一些格点,其到达不了任何同色边缘点。

这很显然,如果成环就把环中所有点全部染成环外节点的颜色即可。

Observation 2.分界线的两端不可能位于同一条边或是相邻的两条边上。

这比较显然,因为显然我们可以把它平移,使得在答案不降的前提下分界线与板子的边缘重合。

Observation 3.分界线必定是连接两相对边的直线。

由 Observation 2 知其必定连接两对边。平移就能得到其必然是直线。

那么设 f(i,0/1) 表示第 i 行是红点/蓝点时的最小割。直接DP即可做到 O(n)

需要注意的是行列上要各做一遍DP。

代码:

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

XVII.CF1368H2 Breadboard Capacity (hard version)

现在加上了修改。但是发现状态很少,于是写成矩阵转移,然后在线段树上维护即可。

时间复杂度 O(nlogn)

代码倒不难想,但是很恶心,因为翻转有两种可能,所以就要维护四种不同的转移矩阵。然后就是二维的,如果写不好很容易成为一份代码复制两遍的这种又难写又难调试的鬼东西。

按照我的写法需要特判 n,m=1 的情形,忘记了以致于 WA 了一发。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,q;
struct Matrix{
	int a[2][2];
	Matrix(){memset(a,0x3f,sizeof(a));}
	int*operator[](const int&x){return a[x];}
	friend Matrix operator*(Matrix&u,Matrix&v){
		Matrix w;
		for(int i=0;i<2;i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)w[i][j]=min(w[i][j],u[i][k]+v[k][j]);
		return w;
	}
};
#define lson x<<1
#define rson x<<1|1
#define mid ((l+r)>>1)
#define LSON lson,l,mid
#define RSON rson,mid+1,r
#define X x,l,r
struct SEGTREE{
	int sum[400100];
	bool rev[400100];
	void REV(int x,int l,int r){sum[x]=(r-l+1)-sum[x],rev[x]^=1;}
	void pushdown(int x,int l,int r){if(rev[x])REV(LSON),REV(RSON),rev[x]=false;}
	void pushup(int x){sum[x]=sum[lson]+sum[rson];}
	void build(char*s,int x,int l,int r){if(l==r)sum[x]=(s[l]=='R');else build(s,LSON),build(s,RSON),pushup(x);}
	void modify(int x,int l,int r,int L,int R){
		if(l>R||r<L)return;
		if(L<=l&&r<=R)REV(X);
		else pushdown(X),modify(LSON,L,R),modify(RSON,L,R),pushup(x);
	}
	int query(int x,int l,int r,int P){
		if(l==r)return sum[x];
		pushdown(X);if(P<=mid)return query(LSON,P);else return query(RSON,P);
	}
}ss[2],tt[2];
#undef RSON
#define RSON rson,mid,r
struct SegTree{Matrix tr[2][2];bool tag[2];}S[400100],T[400100];
void pushup(SegTree*seg,int x){for(int i=0;i<2;i++)for(int j=0;j<2;j++)seg[x].tr[i][j]=seg[lson].tr[i][j]*seg[rson].tr[i][j];}
void REV(SegTree*seg,int x,int sd){
	if(sd==0){seg[x].tag[0]^=1;for(int i=0;i<2;i++)swap(seg[x].tr[0][i],seg[x].tr[1][i]);}
	if(sd==1){seg[x].tag[1]^=1;for(int i=0;i<2;i++)swap(seg[x].tr[i][0],seg[x].tr[i][1]);}
}
void pushdown(SegTree*seg,int x){for(int i=0;i<2;i++)if(seg[x].tag[i])REV(seg,lson,i),REV(seg,rson,i),seg[x].tag[i]=false;}
void build(SegTree*seg,SEGTREE*ges,int x,int l,int r,int N,int M){
	if(l+1==r){
		int _0=ges[0].query(1,1,N,r),_1=ges[1].query(1,1,N,r);
		for(int i=0;i<2;i++)for(int j=0;j<2;j++){
			for(int I=0;I<2;I++)for(int J=0;J<2;J++)seg[x].tr[i][j][I][J]=(I==J?0:M);
			for(int k=0;k<2;k++)seg[x].tr[i][j][k][_0^i]++;
			for(int k=0;k<2;k++)seg[x].tr[i][j][k][_1^j]++;
		}
	}else build(seg,ges,LSON,N,M),build(seg,ges,RSON,N,M),pushup(seg,x);
}
void modify(SegTree*seg,int x,int l,int r,int L,int R,int sd){
	if(L>r||R<=l)return;
	if(L<=l+1&&r<=R)REV(seg,x,sd);
	else pushdown(seg,x),modify(seg,LSON,L,R,sd),modify(seg,RSON,L,R,sd),pushup(seg,x);
}
char STR[100100];
int calc(){
	int res=0x3f3f3f3f;
	static int f[2],g[2];
	f[1]=tt[0].sum[1];
	f[0]=m-f[1];
	f[ss[0].query(1,1,n,1)]++;
	f[ss[1].query(1,1,n,1)]++;
	if(n!=1)g[0]=min(f[0]+S[1].tr[0][0][0][0],f[1]+S[1].tr[0][0][1][0]);else g[0]=f[0];
	if(n!=1)g[1]=min(f[0]+S[1].tr[0][0][0][1],f[1]+S[1].tr[0][0][1][1]);else g[1]=f[1];
	g[1]+=tt[1].sum[1];
	g[0]+=m-tt[1].sum[1];
	res=min(res,min(g[0],g[1]));
	
	f[1]=ss[0].sum[1];
	f[0]=n-f[1];
	f[tt[0].query(1,1,m,1)]++;
	f[tt[1].query(1,1,m,1)]++;
	if(m!=1)g[0]=min(f[0]+T[1].tr[0][0][0][0],f[1]+T[1].tr[0][0][1][0]);else g[0]=f[0];
	if(m!=1)g[1]=min(f[0]+T[1].tr[0][0][0][1],f[1]+T[1].tr[0][0][1][1]);else g[1]=f[1];
	g[1]+=ss[1].sum[1];
	g[0]+=n-ss[1].sum[1];
	res=min(res,min(g[0],g[1]));
	return res;
}
int main(){
	scanf("%d%d%d",&n,&m,&q);
	scanf("%s",STR+1),ss[0].build(STR,1,1,n);
	scanf("%s",STR+1),ss[1].build(STR,1,1,n);
	scanf("%s",STR+1),tt[0].build(STR,1,1,m);
	scanf("%s",STR+1),tt[1].build(STR,1,1,m);
	if(n!=1)build(S,ss,1,1,n,n,m);
	if(m!=1)build(T,tt,1,1,m,m,n);
	printf("%d\n",calc());
	for(int i=1,l,r;i<=q;i++){
		scanf("%s%d%d",STR,&l,&r);
		if(STR[0]=='L')ss[0].modify(1,1,n,l,r),modify(S,1,1,n,l,r,0);
		if(STR[0]=='R')ss[1].modify(1,1,n,l,r),modify(S,1,1,n,l,r,1);
		if(STR[0]=='U')tt[0].modify(1,1,m,l,r),modify(T,1,1,m,l,r,0);
		if(STR[0]=='D')tt[1].modify(1,1,m,l,r),modify(T,1,1,m,l,r,1);
		printf("%d\n",calc());
	}
	return 0;
}

XVIII.[AGC049D]Convex Sequence

这个限制好怪哦。

一看题名 Convex Sequence,哦,原来它就等价于这个序列是的。

凹的就意味着是单谷的。单谷的就意味着可以从谷值处切两半,化作两个相等的子问题求解。

一个 Approach 就是令序列中所有元素全都减去谷值——显然,此时所有元素的和就变成了 mkn,其中 k 是谷值,而与此同时凹性仍然满足。

那么一个 idea 就产生了:我们强制令谷值为 0,然后计算所有 mm(modn)mm 时的答案即可。

考虑将其截为三节:左部递减部分、中间的谷部(连续的 0 段)、右部递增部分。

然后我们发现,对于左右部,它们和最小的情形下也只能是 1,2,3,,而其前缀和是一个 n2 级别的函数。这就意味着其长度不应该长于 m

那就非常好办了。考虑DP求出右部序列在长为 i、和为 j 时的方案数(显然左部序列类似)为 fi,j,对二阶差分后的序列(恒非负)DP。

每次在序列头插入一个非负数,若插入后长度是 i,则这个数在和中共出现了 (i+12)次。可以用完全背包DP。

需要注意的是,二次差分序列开头的数须非负,这意味着我们需要另开一维 0/1 来记录。

我们现在已经DP完了。考虑合并答案。我们枚举左半边的长度与和,则能与其匹配的右半边长度须不大于某值,和须不大于某值且同余于某值。二维前缀和即可。

时间复杂度 O(mm)(与 n 无关)。

需要注意的是,左右部都可能为空,记得特判。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int lim=450;
int n,m,f[452][100100][2],res;
int main(){
	scanf("%d%d",&n,&m);
	f[0][0][1]=1;
	for(int i=1;i<=lim;i++){
		for(int j=0;j<=m;j++)f[i][j][0]=(f[i-1][j][0]+f[i-1][j][1])%mod;
		int val=i*(i+1)/2;
		if(val>m)continue;
		for(int k=val;k<=m;k++)f[i][k][1]=(0ll+f[i][k-val][1]+f[i-1][k-val][0]+f[i-1][k-val][1])%mod;
//		for(int j=0;j<=m;j++)printf("%d ",f[i][j][0]);puts("");
//		for(int j=0;j<=m;j++)printf("%d ",f[i][j][1]);puts("");
	}
	for(int i=0;i<=lim;i++){
		for(int j=0;j<=m;j++)f[i][j][0]=f[i][j][1];
		for(int j=n;j<=m;j++)(f[i][j][0]+=f[i][j-n][0])%=mod;
		if(i)for(int j=0;j<=m;j++)(f[i][j][0]+=f[i-1][j][0])%=mod;
	}
	for(int i=0;i<=min(lim,n-1);i++)for(int j=0;j<=m;j++)(res+=1ll*f[i][j][1]*f[min(n-i-1,lim)][m-j][0]%mod)%=mod; 
	printf("%d\n",res);
	return 0;
}

XIX.[AGC050D]Shopping

Observation 1. 游戏至多进行 K 轮。

这很显然,因为 K 轮后所有人要么已经抽到了,要么就知道所有东西都已经被抽到了

Observation 2.在一轮结束后,所有已抽到的人和所有未抽到的人是全等的。

抽到的人全等是无异议的。未抽到的人,因为排除了相同数量的物品,因此也是全等的。

Observation 3.在一轮中间,所有人被分为三类:已抽到、本轮已经排除过、本轮尚未排除过。三类中所有人是无区别的。

那么我们便可以设计DP了:fi,j,k,p​​​ 表示 i​​​ 轮时 j​​​ 个人已排除、k​​​ 个人未排除、剩下人已抽到,且当前关心的人(必须是一个还没抽到的人)排名是 p​(0p<j+k​​​)时,这个人最终抽到的概率。

用记忆化搜索即可做到 n4

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int n,m,f[50][50][50][50],INV[50];
int dfs(int i,int j,int k,int p){
	if(i>m)return 0;
	int&ret=f[i][j][k][p];if(ret!=-1)return ret;
	ret=0;
	int FAL=1ll*(n-j-k-i+1)*INV[m-i+1]%mod;
	int SUC=(1+mod-FAL)%mod;
	if(SUC){//succeed.
		if(p){
			if(k==1)(ret+=1ll*dfs(i+1,0,j+k-1,p-1)*SUC%mod)%=mod;
			else(ret+=1ll*dfs(i,j,k-1,p-1)*SUC%mod)%=mod;				
		}else(ret+=SUC)%=mod;
	}
	if(FAL){//fail.
		if(k==1)(ret+=1ll*dfs(i+1,0,j+k,p?p-1:j+k-1)*FAL%mod)%=mod;
		else (ret+=1ll*dfs(i,j+1,k-1,p?p-1:j+k-1)*FAL%mod)%=mod;
	}
//	printf("%d %d %d %d[%d,%d]:%d\n",i,j,k,p,FAL,SUC,ret);
	return ret;
}
int main(){
	scanf("%d%d",&n,&m),memset(f,-1,sizeof(f));
	INV[1]=1;for(int i=2;i<=m;i++)INV[i]=1ll*INV[mod%i]*(mod-mod/i)%mod;
	for(int x=0;x<n;x++)printf("%d\n",dfs(1,0,n,x));
	return 0;
}

XX.CF67C Sequence of Balls

插入/删除/替换是 trival 的。

考虑交换。

性质保证同一个元素仅会被交换一次。

考虑交换与其它操作的结合。

先交换再插入是可行的;先删除后交换是可行的;交换且替换,推式子可得如果交换更便宜那么删除再加入还要更便宜,而如果替换更便宜那么两次替换还要更便宜,反正总之不会作用在同一个元素上。

发现不止是两种操作的结合,广义的情形是先删除到两个元素相邻,再交换,再插入。

发现这种情况下有效的是 sa,sbtc,td 中,sa=td,sb=tc 的情形。

这样子交换的话,发现肯定是交换到两个元素在此之前首次出现的位置,预处理一下,然后 nm DP即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int I,D,R,S,n,m;
char s[4010],t[4010];
int f[4010][4010],a[4010][26],b[4010][26];
void chmn(int&x,int y){if(x>y)x=y;}
int main(){
	scanf("%d%d%d%d%s%s",&I,&D,&R,&S,s+1,t+1),n=strlen(s+1),m=strlen(t+1);
	memset(f,0x3f,sizeof(f));
	for(int i=2;i<=n;i++){for(int j=0;j<26;j++)a[i][j]=a[i-1][j];a[i][s[i-1]-'a']=i-1;}
	for(int i=2;i<=m;i++){for(int j=0;j<26;j++)b[i][j]=b[i-1][j];b[i][t[i-1]-'a']=i-1;}
	for(int i=0;i<=n;i++)f[i][0]=i*D;
	for(int i=0;i<=m;i++)f[0][i]=i*I;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){
		chmn(f[i][j],f[i-1][j]+D);
		chmn(f[i][j],f[i][j-1]+I);
		chmn(f[i][j],f[i-1][j-1]+((s[i]!=t[j])?R:0));
		int x=a[i][t[j]-'a'],y=b[j][s[i]-'a'];
		if(x&&y)chmn(f[i][j],f[x-1][y-1]+D*(i-x-1)+I*(j-y-1)+S);
	}
	printf("%d\n",f[n][m]);
	return 0;
} 

XXI.[ARC125F]Tree Degree Subset Sum

Observation 1.本题完全等价于背包问题。

显然任意合法度数序列都能生成树。合法判定是所有度数为正且和为 2n2

这不好。所有元素全都减一。合法判定是所有度数非负且和为 n2

Hypothesis 1.对于每种度数和,其对应的点数为一个区间。

下考虑证明。

考虑 0 的数量,设其为 z。考虑 2 的数的数量。设其为 t。则显然 zt+2,不然和大于 n2

现考虑没有 1 的情形。现考虑某个数 x,设其可以被 y 个度数 2 的点表示出来。

f(x) 为可行点数构成的集合。则,必有 [y,y+z]f(x)

显然有 ytz。这意味着所有可能的 y 对应的区间两两有交。

显然 f(x) 为所有可能的 y 区间之并。因其两两有交,则并亦为区间。

f(x) 为区间。

现考虑有 1 的情形,设其个数为 o。设 g(x) 为此时的可行点数集合。

考虑所有 f(x)​ 非空的 x​。只需证明对于 f(x)​ 与其之后所有非空的 f(y)​,将 f(x)​ 向右平移 yx​ 后与 f(y)​ 仍然有交即可。

这等价于平移后的 f(x) 的左端点不右于 f(y) 的右端点。

f(x)​ 的左端点不可能大于 x/2​​。x/2+(yx)=yx/2​。

而显然 f(y) 的右端点不小于 z。而显然 yx/2yz

平移后的左端点仍小于右端点,这意味着有交。显然 g(y) 为所有这样的 f(x) 平移后的并。每个都与 f(y) 有交进而并为区间。

那么 g(y) 为区间。证毕。

Observation 2.g(x)=ng(n2x)

因为 g(x) 中所有东西全都取反(即选变成不选,不选变成选)即可得到 g(n2x)

那么我们只需求出对于所有 xg(x) 的左端点,右端点即可直接推出,进而区间长度亦可得知。

那么我们就重定义 g(x) 为最小数量的满足和为 x​ 的点数。那么就可以背包了。

因为和为 n2,所以不同的大小数不超过 n。那么就压一下,变成多重背包问题。

多重背包问题就可以单调队列优化至 nn

但是那玩意我不会。所以使用二进制分组,复杂度为 nnlogn……吗?

事实证明完全没有这么大。大约的确是 nn 的。可以轻松跑过,据他人实测甚至跑得比单调队列快?

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,d[200100],c[200100],f[200100];
ll res;
int main(){
	scanf("%d",&n);
	for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),d[x]++,d[y]++;
	for(int i=1;i<=n;i++)c[--d[i]]++;
//	for(int i=0;i<=n;i++)printf("%d ",c[i]);puts("");
	memset(f,0x3f,sizeof(f)),f[0]=0;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=17;j++){
			if(c[i]<(1<<j))break;
			for(int k=n-2;k>=(i<<j);k--)f[k]=min(f[k],f[k-(i<<j)]+(1<<j));
			c[i]-=(1<<j);
		}
		if(c[i])for(int k=n-2;k>=i*c[i];k--)f[k]=min(f[k],f[k-i*c[i]]+c[i]);
	}
	for(int i=0;i<=n-2;i++)if(f[i]!=0x3f3f3f3f&&f[n-2-i]!=0x3f3f3f3f)res+=abs(n-f[n-2-i]-f[i])+1;
	printf("%lld\n",res);
	return 0;
}

XXII.CF1338D Nested Rubber Bands

Observation 1.答案序列中不可能存在两个有边的点。

这是显然的。

Observation 2.对于一个点 x,其不可能存在三个不同的儿子 a,b,c​​,满足在它们的子树中除它们之外还有其它点在答案序列中

设答案序列中三个点分别是 A,B,C​,且在序列中的顺序是 A​ 套在最外,C​ 在最里层(可以把它看作三个同心圆)。因为 a,b,c 三个子树无交,所以 a 侧子树的所有图形必然在 B 圈之外,包括 a 本身;同理,c 侧亦必在 B 圈之内,包括 c 本身。

考虑 x。其与 B 内的 cB 外的 a 同时有交且与 B 无交。这可能吗?不可能。

那么,不存在这种情形时,是否序列就合法呢?

是的。

考虑先确定答案序列的画法——一堆套在一起的同心圆。其中,相邻的一些同心圆同时是某个点的儿子,此时这个点同时与这些点有交。或者,它们也可以被一条链连接着。

于是我们发现,答案序列即为一条链以及所有与其相邻的点构成的导出子图上的最大独立集。

考虑 DP。设 fi,0 表示 i 在链上且不在独立集时的答案,fi,1 则表示在独立集时的答案。DP 时合并父亲与儿子的链拼成完整的链即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,f[100100][2],res;
vector<int>v[100100];
void dfs(int x,int fa){
	f[x][1]=1,f[x][0]=v[x].size();
	for(auto y:v[x]){
		if(y==fa)continue;
		dfs(y,x);
		res=max(res,f[x][1]+f[y][0]-1);
		res=max(res,f[x][0]+f[y][1]-1);
		res=max(res,f[x][0]+f[y][0]-2);
		f[x][1]=max(f[x][1],f[y][0]);
		f[x][0]=max(f[x][0],f[y][1]+(int)v[x].size()-1);
		f[x][0]=max(f[x][0],f[y][0]+(int)v[x].size()-2);
	}
	res=max(res,f[x][0]);
	res=max(res,f[x][1]);
}
int main(){
	scanf("%d",&n);
	for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
	dfs(1,0);
	printf("%d\n",res);
	return 0;
}

XXIII.kurumi

题意:

给定 n 个长为 m 的数字串。现在随机重新排列每个数字串,所有可能的排列方式都是等概率的,求所有数字串建成的字典树的期望点数。对 998244353 取模。

数据范围:1n10,1m100

假如从串去构建字典树是没有出路的;事实上最好的方式是判定每个串是否在字典树中。

设第 i 个串中数字 x 的出现次数是 si(x),设待判定串中的次数是 ti(x),则 tsi 中出现的概率就是

si(x)t(x)_(si(x))t(x)_

其中下横线意为下降幂。

因为显然 si(x)=m,故上式转为

si(x)t(x)_mt(x)_

t 在至少一个 si 中出现的概率即是

1ni=1(1si(x)t(x)_mt(x)_)

拆开得

S{1,2,,n}(1)|S|1iS(si(x)t(x)_mt(x)_)

这样看来,我们只需要记录一个东西,也即 t(x) 即可对每个数字进行背包。

注意到某个 t 事实上总共对应了 (t(x))!t(x)! 种不同的串。下面的东西亦仅与 t(x) 有关,上面的东西在背包同时一并维护。

分析一下复杂度,是 |α|2nm2,其中 α 是值域。

别忘记加上根节点的 1

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int n,m,f[110],s[10][10],res,fac[110],inv[110];
char S[110];
int Falling(int x,int y){if(x<y)return 0;return 1ll*fac[x]*inv[x-y]%mod;} 
int main(){
	freopen("kurumi.in","r",stdin);
	freopen("kurumi.out","w",stdout);
	scanf("%d%d",&n,&m);
	fac[0]=1;for(int i=1;i<=m;i++)fac[i]=1ll*fac[i-1]*i%mod;
	inv[m]=ksm(fac[m]);for(int i=m;i;i--)inv[i-1]=1ll*inv[i]*i%mod; 
//	for(int i=0;i<=m;i++)printf("%d ",fac[i]);puts("");
//	for(int i=0;i<=m;i++)printf("%d ",inv[i]);puts("");
	for(int i=0;i<n;i++){
		scanf("%s",S);
		for(int j=0;j<m;j++)s[i][S[j]-'0']++;
//		for(int j=0;j<10;j++)printf("%d ",s[i][j]);puts("");
	}
	for(int i=1;i<(1<<n);i++){
		f[0]=1;
		for(int j=0;j<10;j++)for(int k=m;k;k--)for(int p=0;p<k;p++){
			int val=1ll*inv[k-p]*f[p]%mod;
			for(int t=0;t<n;t++)
				if(i&(1<<t))
					val=1ll*val*Falling(s[t][j],k-p)%mod;
			f[k]=(val+f[k])%mod;	
		}
		int sum=0;
//		for(int k=1;k<=m;k++)printf("%d ",f[k]);puts("");
		for(int k=1;k<=m;k++)sum=(1ll*f[k]*ksm(ksm(Falling(m,k),__builtin_popcount(i)))%mod*fac[k]+sum)%mod,f[k]=0;
		if(__builtin_popcount(i)&1)(res+=sum)%=mod;else(res+=mod-sum)%=mod;
	}
	printf("%d\n",(res+1)%mod);
	return 0;
}

XXIV.[AGC022E] Median Replace

首先将所有合并分为四类:000001011111

000 的操作显然是能搞就搞。111 的操作显然除非一个 0 都不剩了,否则不会搞。而剩下两种均可以看作是一个 0 和一个 1 同归于尽。于是执行完所有 000 后,比较剩下 01 的数量即可。

但需要注意的是,在后来的过程中也可能产生新的 000。例如 1001001,此时不存在任何 000,但是对中间的 1 合并一次得到 10001,然后进一步得到 101,这样这个序列是合法的。

于是我们得到策略:从前往后遍历整个数组,并维护一个栈。如果并非当前元素是 1、栈顶元素是 0,则入栈。否则,将栈顶三个元素合并。并且,如果任意时刻栈顶有三个 0,合并。

于是我们需要记录如下信息:当前数列中 10 数量之差,以及当前栈顶 0 的数量(不超过 2)。

如果 10 多两个,那么直接把剩下后缀中所有元素全都合并,然后剩下的还是会是 1。反之,如果 01 多两个以上,则通过合并也可以变成不超过两个的情形。

综上,两维的大小均是 O(1) 的,故总复杂度 O(n)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
char s[300100];
int n,f[300100][10][10],res;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int main(){
	scanf("%s",s+1),n=strlen(s+1);
	f[0][2][0]=1;
	for(int i=0;i<n;i++)for(int j=-2;j<=2;j++)for(int k=0;k<=2;k++){
		if(!f[i][j+2][k])continue;
		if(s[i+1]!='1'){
			if(j==2)ADD(f[i+1][j+2][k],f[i][j+2][k]);
			else if(k<2)ADD(f[i+1][j+1][k+1],f[i][j+2][k]);
			else ADD(f[i+1][j+3][k-1],f[i][j+2][k]);
		}
		if(s[i+1]!='0'){
			if(j==2)ADD(f[i+1][j+2][k],f[i][j+2][k]);
			else if(k)ADD(f[i+1][j+3][k-1],f[i][j+2][k]);
			else ADD(f[i+1][j+3][k],f[i][j+2][k]);
		}
	}
	for(int k=0;k<=2;k++)ADD(res,f[n][3][k]),ADD(res,f[n][4][k]);
	printf("%d\n",res);
	return 0;
}

XXV.[POI2015]CZA

p3 一般意味着状压 DP。但是环状结构不能直接 DP。

事实上,我们可以考虑分类讨论。

p=0 时显然仅在 n=1 时答案为 1

p=1 时仅在 n=1,2 且没有特殊限制时为 1

p=2 时考虑 n 两端仅可能为 n1,n2,而 n1 另一端仅可能为 n3,故最终构成了如下结构或其反结构的方案:

n-(n-1)-(n-3)-(n-5)-...
|                     |
(n-2)-(n-4)-(n-6)-... 1

故仅需考虑 p=3

此时考虑从 n 开始把每个元素依次插入环。x 能插入的位置仅可能是 x+1,x+2,x+3 三者间,且它们的顺序仅可能是顺时针或逆时针两种,于是就这样 DP 即可。复杂度事实是 np!2p

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,p,a[1001000];
vector<int>v[1001000],u[1001000];
#define Uf(x,y) binary_search(u[x].begin(),u[x].end(),y)
#define Vf(x,y) binary_search(v[x].begin(),v[x].end(),y)
bool chea(){for(int i=0;i<n;i++)if(Uf(a[i],a[(i+1)%n]))return false;return true;}
int f[1001000][2][8];
int main(){
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),u[y].push_back(x);
	for(int i=1;i<=n;i++)sort(v[i].begin(),v[i].end()),sort(u[i].begin(),u[i].end());
	if(n==1){puts("1");return 0;}
	if(p==0){puts("0");return 0;}
	if(n==2){printf("%d\n",!m);return 0;}
	if(p==1){puts("0");return 0;}
	if(p==2){
		int c=0,ret=0;for(int i=n-1;i>=1;i-=2)a[c++]=i;reverse(a,a+c);
		a[c++]=n;for(int i=n-2;i>=1;i-=2)a[c++]=i;
//		for(int i=0;i<n;i++)printf("%d ",a[i]);puts("");
		ret+=chea();
		reverse(a,a+n);
		ret+=chea();
		printf("%d\n",ret);
		return 0;
	}
	f[n-2][0][7]=f[n-2][1][7]=1;
	//0:x+2,x+1 1:n+1,x 2:x,x+2;
	for(int i=n-3;i;i--)for(int j=0;j<2;j++)for(int k=0;k<8;k++){
		if(!f[i+1][j][k])continue;
//		printf("%d,%d,%d:%d\n",i+1,j,k,f[i+1][j][k]);
		for(int t=0;t<3;t++){
			if(!(k&(1<<t)))continue;
			int _0,_1,_2;
			if(t==0){
				_0=(k>>1)&1,_1=0,_2=1;
				if(j==0&&((k&4)&&Uf(i+1,i+3)||Uf(i+3,i)))continue;
				if(j==1&&((k&4)&&Vf(i+1,i+3)||Vf(i+3,i)))continue;
				(f[i][j][_0|_1<<1|_2<<2]+=f[i+1][j][k])%=mod;
			}
			if(t==1){
				_0=0,_1=1,_2=1;
				if(j==0&&((k&4)&&Uf(i+1,i+3)||(k&1)&&Uf(i+3,i+2)))continue; 
				if(j==1&&((k&4)&&Vf(i+1,i+3)||(k&1)&&Vf(i+3,i+2)))continue; 
				(f[i][!j][_0|_1<<1|_2<<2]+=f[i+1][j][k])%=mod;
			}
			if(t==2){
				_0=(k>>1)&1,_1=1,_2=0;
				if(j==0&&(Uf(i,i+3)||(k&1)&&Uf(i+3,i+2)))continue;
				if(j==1&&(Vf(i,i+3)||(k&1)&&Vf(i+3,i+2)))continue;
				(f[i][j][_0|_1<<1|_2<<2]+=f[i+1][j][k])%=mod;
			}
		}
	}
	int res=0;
	for(int j=0;j<2;j++)for(int k=0;k<8;k++){
		if(j==0){
			if((k&1)&&Uf(3,2))continue;
			if((k&2)&&Uf(2,1))continue;
			if((k&4)&&Uf(1,3))continue;
		}
		if(j==1){
			if((k&1)&&Vf(3,2))continue;
			if((k&2)&&Vf(2,1))continue;
			if((k&4)&&Vf(1,3))continue;
		}
		(res+=f[1][j][k])%=mod;
	}
	printf("%d\n",res);
	return 0;
} 

XXVI.CF889E Mod Mod Mod

我们考虑令 xi 表示第 i 项的贡献,即 xi=Xmoda1moda2modmodai

现在考虑令 X 减一。发现,如果 xi0(显然满足该性质的 i 构成序列的一段前缀)则必有 xi 减小一。

对于满足 xi0i,我们令 Y=ij=1(xjxi)。则,只要 X 的减小量 Δ 不超过 xi,就必有 Y 不变。即,若 XXxi+v,则 ij=1xiY+iv

fi,j 表示 xi=j 时最大的 Y。枚举 vXXxi+v,则 fi,j+vi(vmodai+1)ifi+1,vmodai+1。其中,vi 是改变 Xij=1xi 的影响,(vmodai+1)i 是由 ij=1xi 推到 i+1 处的 Y 时的减量。

发现,对于某个 j,不同的转移可能有着相似的效果。事实上,我们只需尝试两种可能的转移即可:一种令 v=jai+1ai+11,此时 vmodai+1 的值被最大化;一种就是 jmodai+1。其它状态要么更劣(例子是不来自于最后一个周期的转移),要么可以在之后被得到(因为这两种转移最大化了 xi+1,且之后的转移有统一减小之前的 x 的选项)。

每次执行转移时,仅考虑不小于 ai+1j,则其至少减半。故一个 j 在至多 log 次内就会衰减至 0,而新产生的 j 都是 ai+11 的形式,故总状态数是对数的。用 map 维护,复杂度对数平方。

本方法的理念和整数划分的平方做法类似,也是通过定义状态让“靠后的状态可以修改靠前状态的值,且修改的效果可以被简单计算”进而处理问题。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
ll a[200100];
map<ll,ll,greater<ll> >mp;
void chmx(ll&x,ll y){if(x<y)x=y;}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),a[n+1]=1;
	mp[a[1]-1]=0;
	for(int i=1;i<=n;i++)for(auto it=mp.begin();it!=mp.end();it=mp.erase(it)){
		if(it->first<a[i+1])break;
		chmx(mp[it->first%a[i+1]],it->second+it->first*i-it->first%a[i+1]*i);
		chmx(mp[a[i+1]-1],it->second+((it->first+1)/a[i+1]-1)*a[i+1]*i);
	}
	printf("%lld\n",mp[0]);
	return 0;
}

XXVII.XorSum

判定长度为 n、和为 s、异或和为 x​​ 的非负整数序列是否存在。若不存在,输出 1,否则输出序列中最大值的最小值。

数据范围:0n,s,x<260​​。不超过 200 组数据。

首先可以找到数组 b​​ 表示第 i​​ 位上需要 bi​​ 个一,此时满足和为 s,异或和为 x

假如 b​ 被确定,则二分即可得到答案。

现在问题是怎么确定 b。我们可以构造出字典序最小的数组 b0,则一切 b 都可以通过 b0 出发,令 bi 减少 2 并令 bi1 增加 4 得到。

b0​ 的构造是简单的。

现在考虑求解答案。二分一个 m,并设 fi,j 表示当前确定了高 i 位,且有 j 个元素确定小于 m 时,最小要下传的个数。

bi 的实际值是 b0i+fi,j,设为 k

m 的第 i 位是 0,则只有 j 个小于 m 的数这位可以填一,把 2max(kj,0) 转移到 fi1,j

若是 1,则我们可以通过让若干个等于 m 的数填 0 来转移到更大的 j

这个 DP 的 i 一维是 log 的,但是 j 一维乍一看是 n 的。但是因为 b0 的初值全部 <4,所以我们一次至多转移到 j 增加 O(1) 的态,进而 j 一维便压缩到了 O(log)。这样,验证的复杂度就是 log2 的。再加上外层的二分,复杂度是三方对数。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T;
ll n,s,x,num[70];
ll f[70][70];
bool che(ll m){
//	printf("%lld:\n",m);
	memset(f,0x2e,sizeof(f)),f[61][0]=0;
	for(int i=60;i>=0;i--)for(int j=0;j<=min(n,60ll);j++){
		if(!f[i+1][j]&&j>=3)return true;
		ll K=num[i]+f[i+1][j];K=max(K-j,0ll);
//		printf("%d,%d:%lld\n",i,j,K);
		if(!((m>>i)&1)){
			if(K&1)K++;
			if(K>num[i]+f[i+1][j])continue;
			f[i][j]=min(f[i][j],K<<1);
		}else for(int J=j;J<=min(n,60ll);J++){
			ll KK=max(K-(n-J),0ll);
			if(KK&1)KK++;
			if(KK>num[i]+f[i+1][j])continue;
//			printf("SPP%d,%d:%lld\n",i,J,KK);
			f[i][J]=min(f[i][J],KK<<1);
		}
	}
	for(int j=0;j<=min(n,60ll);j++)if(!f[0][j])return true;
	return false;
}
ll mina(){
	scanf("%lld%lld%lld",&n,&s,&x);
	if(x>s||((x^s)&1))return -1;
	for(int i=0;i<=60;i++)num[i]=((s>>i)&1)-((x>>i)&1);
	for(int i=60,j;i>=0;i--){
		if(num[i]>=0){if(num[i]&1)num[i]--,num[i-1]+=2;continue;}
		for(j=i+1;!num[j];j++);
		for(;j>i;j--)num[j]-=2,num[j-1]+=4;
		num[i]--,num[i-1]+=2;
	}
	for(int i=0;i<=60;i++)if((x>>i)&1)num[i]++;
	ll mx=0;for(int i=0;i<=60;i++)mx=max(mx,num[i]);
	if(n<mx)return -1;
//	for(int i=0;i<5;i++)printf("%d ",num[i]);puts("");
	ll l=0,r=s;
	while(l<r){
		ll mid=(l+r)>>1;
		if(che(mid))r=mid;else l=mid+1;
	}
	return l;
}
int main(){
	freopen("xs.in","r",stdin);
	freopen("xs.out","w",stdout);
	scanf("%d",&T);
	while(T--)printf("%lld\n",mina());
	return 0;
}

XXVIII.[CodeChef]Queue at the Bakery

首先我们可以给员工也排个队,当一个员工处理完一个顾客后就排到队尾。然后发现,此时一个员工处理的就是所有标号模 mi 的顾客。

这个观察非常重要,于是我们接下来就可以对每个员工分开处理了。 设 fi,j,k 表示在时刻 i,一个员工手头上的顾客已经排到了 j,且他是队列中第 k​ 个员工时,期望等待时长。

考虑 ii+1​​,此时有两种可能,也即来新顾客或者没来。分开来转移即可。

分析一下复杂度。i,j,k 三维分别要枚举到 n,nd,m;这看上去是个非常荒诞的复杂度。但是首先一个观察是当 j>n 时,其已经不可能在接下来的时间中减到零了,故直接按照等差数列算即可。事实上,上界取一个 1500 即可,因为 md 时,积攒到 1500 的等待时间本身就是一个极小概率事件;m<d 时,一旦成功积攒到 1500,把它消到 0(这意味着在接下来 1500 的时刻中不出现超过 m 个人)就成为了极小概率事件。因而直接按照 1500 算,其之后用等差数列处理即可。

复杂度 O(1500nm)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,d;
double p,f[2][1510][10],pro[50100][10],res;
double expe(int i,int j,int k){
	if(j<=1500)return f[i&1][j][k];
	return f[i&1][1500][k]+pro[i][k]*(j-1500);
}
int main(){
	scanf("%d%d%d%lf",&n,&m,&d,&p);
	for(int i=1;i<=n;i++)for(int j=0;j<m;j++){
		pro[i][j]=(1-p)*pro[i-1][j];
		if(j)pro[i][j]+=p*pro[i-1][j-1];
		else pro[i][j]+=p*(pro[i-1][m-1]+1);
	}
	for(int i=1;i<=n;i++)for(int j=0;j<=1500;j++)for(int k=0;k<m;k++){
		f[i&1][j][k]=(1-p)*f[!(i&1)][max(0,j-1)][k];
		if(k)f[i&1][j][k]+=p*f[!(i&1)][max(0,j-1)][k-1];
		else f[i&1][j][k]+=(expe(i-1,j+d-1,m-1)+j)*p;
	}
	for(int i=0;i<m;i++)res+=f[n&1][0][i];
	printf("%.10lf\n",res);
	return 0;
} 

XXIX.CF1349E Slime and Hats

考虑 n 号人。若其在第一轮没有坐下,这意味着前 n1 个人中存在黑帽子,并且这个消息前 n1 个人都知道。

现在,若 n1 号人在第二轮没有坐下,则意味着前 n2 个人中存在黑帽子……

ni+1 号人在第 i 轮坐下了,这意味着所有其之后的人都是白帽子。因此,所有其之后且未坐下的人都会在第 i+1 轮坐下。

并且,第 i 轮其坐下后,会重新从第 n 个人开始一轮判定。不停判定,直到某一时刻,所有黑帽人都坐下了,那么下一时刻所有人都会坐下。

于是我们现在知道了按照从 n1 的方向,答案序列是 a1+1,,a1+1,a1,a1+1,,a1+1,a2,a2+1,,a2+1,a3,a3+1,a3+1,a4 这样的形式,且 a1,a2,a3, 依次递减。

我们现在已经知道如何由帽子序列生成时间序列了。这可以被用来手写一个 SPJ 来对拍。

现在考虑如何由时间序列构造帽子序列。

考虑把这些确定的位置写成一个序列,称之为“确定序列”。假如确定序列中存在相邻且相等的位置,则这两个位置肯定都在同一个 ai+1 段中,可以合并;假如确定序列中存在相邻且后者的数等于前者的数加一的位置,则前面的数必然是某个 ai,后面的数必然是其对应的 ai+1,也可以合并。

这样合并之后,确定序列中所有位置都属于不同的 ai

考虑从后往前由确定序列构造时间序列。对于确定序列中每个位置,其可能对应 aiai+1(当然,因为我们把某些东西合并了,所以一些位置必须对应 ai,另一些位置必须对应 ai+1)。若其是 ai,则其对应的段已经确定了;若其是 ai+1,我们不知道其对应的 ai 究竟出现在时间序列中的哪里;但是,我们肯定希望这个 ai 出现的下标越靠后越好,因为越靠后就能省出更多位置出来,更有可能找出满足其之前位置的限制。

于是我们设一个 bool 数组 vis 表示每个位置能否对应 ai,以及一个 int 数组 f 表示每个位置对应 ai+1 时,ai 出现的最小下标。

考虑转移。转移的时候,考虑确定序列中相邻两个数,其对应了两段 [ai,ai+1,ai+1,,ai+1][aj,aj+1,aj+1,,aj+1];这两段中间还可能出现一些新段,这样才能把时间从 aj 撑到 ai

这涉及到形如 [l,r,v] 的判定,意为“能否用 [l,r] 中元素作 01 背包凑出 v”。考虑枚举选取其中的 i 个元素,则此时的下界是选择最小的 i 个元素,上界是选择最大的 i 个元素,且上下界间所有值都可以调整得出。故我们只需判定是否有 v 在上下界之间即可。通过二分,我们可以 O(log) 地求出满足下界的最大 i 和满足上界的最小 i,判定二者是否有交即可。当然也可以使用数学方法(解二次方程)O(1) 实现判定。但不管怎么说,我们都已经实现了较快的判定方式。

然后转移就从 visifi 出发,分别转移即可,是简单的。

需要注意的是,在开头和结尾时的转移比较特殊,要特别处理,但也是简单的。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
ll a[200100],b[200100];
void pr(){for(int i=1;i<=n;i++)printf("%lld",b[n-i+1]);puts("");}
void mina(){
	ll t=1;
	for(int i=n;;i--){
		int j=i;
		while(j&&!a[j])j--;
		if(!j){for(int k=1;k<=i;k++)b[k]=t;break;}
		b[j]=t+j-1;
		for(int k=j+1;k<=i;k++)b[k]=t+j;
		t+=j,i=j;
	}
	pr();
}
bool check(int l,int r,ll val){
//	printf("CHECK:[%d,%d,%lld]\n",l,r,val);
	if(val==0)return true;
	if(val<0||(1ll*(r+l)*(r-l+1)>>1)<val)return false;
	int u=l-1,v=r;
	while(u<v){
		int w=(u+v+1)>>1;
		if(val>=(1ll*(l+w)*(w-l+1)>>1))u=w;
		else v=w-1;
	}
	int L=u-l+1;
	u=l,v=r+1;
	while(u<v){
		int w=(u+v+1)>>1;
		if(val<=(1ll*(w+r)*(r-w+1)>>1))u=w;
		else v=w-1;
	}
	int R=r-v+1;
	return L>=R;
}
void func(int l,int r,ll val){
//	printf("FUNC:[%d,%d,%lld]\n",l,r,val);
	if(val==0)return;
	for(int i=1;i<=r-l+1;i++){
		ll lav=val-1ll*i*l;
		if(!(lav>=(1ll*(i-1)*i>>1)&&lav<=(1ll*(r-l+r-l-i+1)*i>>1)))continue;
//		printf("UGUGU:%d,%d\n",i,lav);
		vector<int>w;
		lav-=(1ll*(i-1)*i>>1);
		for(int j=0;j<i;j++)w.push_back(l+j);
		for(int j=i-1;j>=0;j--){
			if(lav<=(r-l+1)-i){w[j]+=lav,lav=0;break;}
			lav-=(r-l+1)-i;
			w[j]+=(r-l+1)-i;
		}
//		for(auto x:w)printf("%d ",x);puts("");
//		if(lav)exit(0);
		assert(!lav);
		for(auto x:w)b[x]=1;
		return;
	}
	assert(0);
}
int P[200100],Q[200100],f[200100],m,mus[200100];
bool F[200100],V[200100];
bool vis[200100];
void amin(){
	bool az=true;memset(mus,-1,sizeof(mus));
	for(int i=1;i<=n;i++)if(a[i]){
		az&=(a[i]<=1);
		if(!m){m++,P[m]=Q[m]=i;continue;}
		if(a[i]==a[P[m]]&&mus[m]!=0){mus[m]=1;Q[m]=i;continue;}
		if(a[i]==a[P[m]]+1){mus[m]=0;Q[m]=i;continue;}
		m++,P[m]=Q[m]=i;
	};
	if(az){for(int i=1;i<=n;i++)b[i]=0;pr();return;}
//	for(int i=1;i<=m;i++)printf("[%d,%d:%lld(%d)]\n",P[i],Q[i],a[P[i]],mus[i]);
	if(m==1){
		if(mus[1]!=1&&check(Q[1]+1,n,a[P[1]]-P[1])){
			b[P[1]]=1,func(Q[1]+1,n,a[P[1]]-P[1]),pr();
			return;
		}
		assert(mus[1]!=0);
		for(int i=1;i<=n;i++)if(!a[i]&&check(max(Q[1],i)+1,n,a[P[1]]-i-1)){
			b[i]=1,func(max(Q[1],i)+1,n,a[P[1]]-i-1),pr();
			return;
		}
		assert(0);
		return;
	}
	memset(f,-1,sizeof(f));
	if(mus[m]!=1&&check(Q[m]+1,n,a[P[m]]-P[m]))vis[m]=true;
	if(mus[m]!=0)for(int i=P[m]-1;i>Q[m-1];i--)if(check(Q[m]+1,n,a[P[m]]-1-i))
		{f[m]=i;break;}
	for(int i=m-1;i>=2;i--){
		if(mus[i]!=1){
			if(vis[i+1]&&check(Q[i]+1,P[i+1]-1,a[P[i]]-a[P[i+1]]-P[i]))
				vis[i]=true,V[i]=false;
			if(f[i+1]!=-1&&check(Q[i]+1,f[i+1]-1,a[P[i]]-(a[P[i+1]]-1)-P[i]))
				vis[i]=true,V[i]=true;
		}
		if(mus[i]!=0)for(int j=P[i]-1;j>Q[i-1];j--){
			if(vis[i+1]&&check(Q[i]+1,P[i+1]-1,a[P[i]]-1-a[P[i+1]]-j))
				{f[i]=j,F[i]=false;break;}
			if(f[i+1]!=-1&&check(Q[i]+1,f[i+1]-1,(a[P[i]]-1)-(a[P[i+1]]-1)-j))
				{f[i]=j,F[i]=true;break;}
		}
	}
	int rp=-1;bool tp;
	if(vis[2])for(int j=P[2]-1;j;j--){
		if(j==P[1]){
			if(mus[1]==1)continue;
			if(check(Q[1]+1,P[2]-1,a[P[1]]-a[P[2]]-j))rp=j,tp=false;
			continue;
		}
		if(a[j]||mus[1]==0)continue;
		if(check(max(Q[1],j)+1,P[2]-1,(a[P[1]]-1)-a[P[2]]-j))rp=j,tp=false;	
	}
	if(f[2]!=-1)for(int j=f[2]-1;j;j--){
		if(j==P[1]){
			if(mus[1]==1)continue;
			if(check(Q[1]+1,f[2]-1,a[P[1]]-(a[P[2]]-1)-j))rp=j,tp=true;
		}
		if(a[j]||mus[1]==0)continue;
		if(check(max(Q[1],j)+1,f[2]-1,(a[P[1]]-1)-(a[P[2]]-1)-j))rp=j,tp=true;
	}
	int i=n+1,j,k;
	if(mus[1]!=0&&a[P[1]]==a[P[2]]+1&&vis[2])j=P[2];
	else{
		i=rp,j=(tp?f[2]:P[2]);
		if(i==P[1])b[P[1]]=1,func(Q[1]+1,j-1,a[P[1]]-(a[P[2]]-(j!=P[2]))-P[1]);
		else b[i]=1,func(max(Q[1],i)+1,j-1,(a[P[1]]-1)-(a[P[2]]-(j!=P[2]))-i);
	}
	for(i=2;i<m;i++,j=k){
		if(j==P[i])k=(V[i]?f[i+1]:P[i+1]);
		else k=(F[i]?f[i+1]:P[i+1]);
		b[j]=1,func(Q[i]+1,k-1,(a[P[i]]-(j!=P[i]))-(a[P[i+1]]-(k!=P[i+1]))-j);
	}
	b[j]=1,func(Q[m]+1,n,(a[P[m]]-(j!=P[m]))-j),pr();
}
#ifndef Troverld 
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[n-i+1]);
	amin();
	return 0;
}
#else
mt19937 rnd(17680321);
int main(){
	for(int t=1;t<=1000;t++){
		n=rnd()%10+1;
		printf("%d:%d\n",t,n);
		for(int i=1;i<=n;i++)a[i]=rnd()&1,b[i]=0;
		for(int i=1;i<=n;i++)printf("%lld ",a[i]);puts("");mina();
		for(int i=1;i<=n;i++)a[i]=((rnd()&1)?b[i]:0),b[i]=0;
		for(int i=1;i<=n;i++)printf("%lld ",a[i]);puts("");
		amin();
		memset(P,0,sizeof(P)),memset(Q,0,sizeof(Q)),m=0,memset(vis,false,sizeof(vis));
		memset(F,false,sizeof(F)),memset(V,false,sizeof(V));
		static int c[200100];
		for(int i=1;i<=n;i++)c[i]=a[i],a[i]=b[i],b[i]=0;mina();
		for(int i=1;i<=n;i++)assert(!c[i]||c[i]==b[i]),c[i]=b[i]=0;
	}
	return 0;
}
#endif

XXX.CF1534G A New Beginning

Claim 1.对点 (Xi,Yi) 打标记的时刻,一定有 x+y=Xi+Yi,其中 (x,y) 为你的坐标。也即,打标记的时刻,你一定在经过 (Xi,Yi) 且与二四象限平分线平行的线上。

考虑假如你还没有走到这条线上,列式可以发现你无论向右走还是向上走,切比雪夫距离都不会增大;同理,假如你以及走过了这条线,无论是向右还是向上都不会减少。故最小值定然发生在该直线上。

于是我们便找到了一个决策的顺序:按照 Xi+Yi 递增的顺序决策每一个点。

那么我们设一个 DP:设 fi,j 表示当前已经决策完前 i 个点,且你的横坐标在 j 时的最小花费。

然后转移是 fi,j=mink[jd,j]{fi1,k}+|Xij|,其中 d 是你到上一层的距离。显然暴力搞是 O(n2) 级别的,不可能过。

我们考虑把它写成一个函数 fi(j):在 i 增大的过程中操作共有两个,一是取 min,二是增加一个函数。

考虑归纳证明它是凸的,也即它的二阶导恒非负。首先对于一个凸函数执行上述取 min 操作只不过是从 min 的位置剖开然后把右边的部分右移 d 的距离罢了;然后增加的函数是 |Xij|,也是凸的。故这整个函数就是凸的。

考虑描述这个函数:它只有在一些“转折点”的位置上二阶导非零,且每经过一个转折点斜率就会加一。于是我们可以用所有的转折点集合,再加上最小值坐标来描述这个函数。取 min 就是把最小值右侧的所有转折点右移 d,插入 |Xij| 就是在 Xi 的位置连插两个转折点。具体而言,我们只需要维护两个堆表示最小值左侧和右侧的转折点,然后插入转折点后暴力做 O(1) 次调整把左右的转折点数量扳平,平移就靠打 tag 即可。

(这种维护两个堆的做法其实是动态维护中位数的常见 trick:明显最小值出现的位置即为转折点的中位数)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
#define x first
#define y second
int n;
pii p[800100];
priority_queue<int>q;
priority_queue<int,vector<int>,greater<int> >r;
ll mn;
int tag;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&p[i].x,&p[i].y);
	sort(p+1,p+n+1,[](const pii&u,const pii&v){return u.x+u.y<v.x+v.y;});
	q.push(p[1].x),r.push(-p[1].y);
	for(int i=2;i<=n;i++){
//		printf("<%d,%d>\n",p[i].x,p[i].y);
		tag=p[i].x+p[i].y;
		if(p[i].x<q.top()){
			mn+=abs(p[i].x-q.top());
			q.push(p[i].x),q.push(p[i].x);
			r.push(q.top()-tag),q.pop();
		}else if(p[i].x>r.top()+tag){
			mn+=abs(p[i].x-tag-r.top());
			r.push(p[i].x-tag),r.push(p[i].x-tag);
			q.push(r.top()+tag),r.pop();
		}else q.push(p[i].x),r.push(p[i].x-tag);
	}
	printf("%lld\n",mn);
	return 0;
}

XXXI.CF1225G To Make 1

注意到你的目标有一组 {bi} 满足 ni=1aikbi=1

那么,是否任何 {bi} 都能反推出一组方案呢?答案是肯定的。

首先因为 a 中不存在 k 的倍数进而存在至少两个 bi 取到了 max{bi}。然后可以合并之,得到一个子问题。

于是我们现在要做的就是构造一组 bi 满足条件即可。

使用背包:fi,j 表示用集合 i 中数背包出 j 是否可行。假如 jk 的倍数,则 jk 也可行(把它们的 bi 同时增大 1 即可)。

分析一下复杂度。背包可以用 bitset 优化,复杂度是 O(n2naω) 的;由 jjk 的过程必然要扫过每个位置,复杂度是 O(2na) 的。

构造是简单的。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,K,a[16],b[16];
bitset<2010>bs[1<<16];
bool era[16];
int main(){
	scanf("%d%d",&n,&K);
	for(int i=0;i<n;i++)scanf("%d",&a[i]);
	bs[0].set(0);
	for(int i=1;i<(1<<n);i++){
		for(int j=0;j<n;j++)if(i&(1<<j))bs[i]|=bs[i^(1<<j)]<<a[j];
		for(int j=2000/K;j;j--)if(bs[i].test(j*K))bs[i].set(j);
	}
	if(!bs[(1<<n)-1].test(1)){puts("NO");return 0;}
	for(int i=(1<<n)-1,k=1,t=0;i;){
		bool fd=false;
		while(!fd){
//			printf("%d,%d,%d\n",i,k,t);
			for(int j=0;j<n;j++)if((i&(1<<j))&&k>=a[j]&&bs[i^(1<<j)].test(k-a[j]))
				{i^=1<<j,b[j]=t,k-=a[j],fd=true;break;}
			if(fd)break;
			k*=K,t++;
		}
	}
	puts("YES");
	for(int i=1;i<n;i++){
		int mx=-1;
//		for(int j=0;j<n;j++)if(!era[j])printf("[%d,%d]",a[j],b[j]);puts("");
		for(int j=0;j<n;j++)if(!era[j])mx=max(mx,b[j]);
		vector<int>v;
		for(int j=0;j<n;j++)if(!era[j]&&mx==b[j])v.push_back(j);
//		if(v.size()<2)exit(0);
		assert(v.size()>=2);
		printf("%d %d\n",a[v[0]],a[v[1]]);
		a[v[0]]+=a[v[1]],era[v[1]]=true;
		while(!(a[v[0]]%K))a[v[0]]/=K,b[v[0]]--;
	}
//	for(int i=0;i<n;i++)printf("%d ",b[i]);puts("");
	
	return 0;
}

XXXII.集合划分计数问题

1n 的整数划分为 m 个集合,满足存在一个 圆排列 p,使得对于相邻的 pi,pi+1,总有第 pi 个集合中的 max 大于第 pi+1 个集合中的 min

两个方案不同,当且仅当存在两个数,在一种方案中处于同一个集合中,另一种方案中则否。

求出不同方案数,对 109+7 取模。

数据范围:n,m500

首先考虑如何判定方案合法。

首先,一个集合可以只用二元组 [min,max] 描述。于是可以将其看作一条线段。

考虑两个线段何时只能单向相连:显然,当且仅当它们无交。此时,只有较大的线段后可以接较小的线段,而不能反过来。

考虑一组极长两两无交的线段集。显然它们在圆排列中的次序如果不考虑其它线段则是固定的,那么我们接下来就只要考虑其它线段的影响罢了。

然后发现,对于两个无交线段,只需要 存在一个与二者都有交的线段 作为 过渡,它们之间就可以以任何次序连接了。

考虑这与什么东西等价。然后进而可以轻松得到结论,也即只需在把每个区间看作是 两端开 的时,满足所有开区间 (1,n) 中的整点都至少位于一个开区间中即可。

有了这个结论就可以设计算法了。

一个 trivial 的想法是大力容斥。也即,钦定首个不被任何开区间包含的位置,然后把它分成一个 合法的前缀任意填的后缀。合法的前缀是子状态,任意填的后缀是第二类斯特林数。

那么暴力 DP 是 O(n4) 的;注意到这是半在线二维卷积的形式,那么可以把它转成二维多项式求逆的形式,可以大力优化到平方对数。

应该也能过了,但是不管怎么说在考场上去写 MTT 什么的也太蠢了。

考虑另一种 DP。fi,j,k 表示当前 DP 到位置 i,有 j 条线段的左右端点均确定,有 k 条线段仅有左端点确定,此时的方案数。转移枚举第 i 个点作为 左端点/右端点/中间点/孤立点 计入状态,复杂度 O(n3)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[510][510][510];
int main(){
	freopen("set.in","r",stdin);
	freopen("set.out","w",stdout);
	scanf("%d%d",&n,&m);
	f[1][0][1]=1;
	for(int i=1;i<n;i++)for(int k=1;k<=m;k++)for(int j=0;j+k<=m;j++){
//		printf("%d,%d,%d:%d\n",i,j,k,f[i][j][k]);
		(f[i+1][j][k+1]+=f[i][j][k])%=mod;//left point.
		(f[i+1][j+1][k-1]+=1ll*f[i][j][k]*k%mod)%=mod;//right point.
		(f[i+1][j][k]+=1ll*f[i][j][k]*k%mod)%=mod;//middle point.
		(f[i+1][j+1][k]+=f[i][j][k])%=mod;//single point.
	}
	printf("%d\n",f[n][m][0]);
	return 0;
}

XXXIII.[CodeChef]Max-digit Tree

加强版:支持 K 进制查询。2K10

首先我们必须要会判定一个数是否出现在序列 a 中。

考虑某个数在 K 进制下的表达,是 ¯vtvt1vt2vt3v1

因为在 a 中每次只会加上一个 [1,K1] 的数,所以这个数一定曾处于 ¯vtvt1vt2vt3v3v2x 的态,然后处于 ¯vtvt1vt2vt3v30x 的态,¯vtvt1vt2vt3v400x,,¯vtvt1vt2vp+2vp+100000x,,¯vt000000x,最终到达 x

于是我们尝试用一个状态来刻画一个态。fi,j,k 表示第 2i 位为 0i+1t 位中最大值为 j,个位值为 k 时,在第 i+1 位产生一位进位后,个位的值会变成什么。这个态是纯粹用于辅助的态,转移从 fi1,j,k 出发跳 k 次即可。初态是 f1,j,k 暴力跳直到进位。

f 数组是用于态间转移时使用的,可以在 O(nK3) 的时间内预处理出。

然后考虑刻画一个态。gi,j,k,l 表示 i+1t 位已经处理完成,且这些位中最大值为 j2i 位为 0;个位为 k;此时,要往第 i 位填入 l 时,k 会变成什么。亦可以 O(nK3) 处理。

考虑判定一个数能否出现。从 (t,0,1) 这个态出发,每次走 l=vi 对应的边,直到走到 (1,?,?) 的态后手动跳 O(K) 步即可。

那么考虑 DP。这种连通块问题的常见解法是,父亲的 DP 数组拷贝给儿子,然后儿子在子树中周游一圈后再加给父亲即可。

于是令 hx,i,j,k 表示在点 x 且处于 (i,j,k) 的态数。转移是 O(1) 的,故复杂度 O(n2K2)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int n,m,h[510][510][12][12],a[510],f[510][12][12],g[510][12][12][12],res;
bool ok[12][12][12];
vector<int>v[510];
void dfs(int x){
	sort(v[x].begin(),v[x].end());
	for(auto y:v[x]){
		v[y].erase(find(v[y].begin(),v[y].end(),x));
		for(int j=0;j<m;j++)for(int k=0;k<m;k++)if(ok[j][k][a[y]])(res+=h[x][1][j][k])%=mod;
		for(int i=2;i<=n;i++)for(int j=0;j<m;j++)for(int k=0;k<m;k++)if(h[x][i][j][k])
			(h[y][i-1][max(j,a[y])][g[i][j][k][a[y]]]+=h[x][i][j][k])%=mod;
		dfs(y);
		for(int i=1;i<=n;i++)for(int j=0;j<m;j++)for(int k=0;k<m;k++)if(h[y][i][j][k])
			(h[x][i][j][k]+=h[y][i][j][k])%=mod;
	}
}
int main(){
	freopen("buried.in","r",stdin);
	freopen("buried.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	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=0;i<m;i++)for(int j=m-1;j>=0;j--)
		if(j+max(i,j)>=m)f[1][i][j]=(j+max(i,j))%m;
		else f[1][i][j]=f[1][i][j+max(i,j)];
	for(int i=2;i<=n;i++)for(int j=0;j<m;j++)for(int k=0;k<m;k++){
		int&x=f[i][j][k];x=k;
		for(int t=0;t<m;t++)x=f[i-1][max(j,t)][x];
	}
	for(int i=2;i<=n;i++){
		for(int j=0;j<m;j++)for(int k=0;k<m;k++)g[i][j][k][0]=k;
		for(int t=1;t<m;t++)for(int j=0;j<m;j++)for(int k=0;k<m;k++)
			g[i][j][k][t]=f[i-1][max(j,t-1)][g[i][j][k][t-1]];
	}
	for(int i=0;i<m;i++)for(int j=0;j<m;j++)if(i||j)for(int k=j;k<m;k++){
		int t=j;
		while(t<k)t+=max(i,t);
		if(t==k)ok[i][j][k]=true;
	}
	res=ok[0][1][a[1]];for(int i=1;i<=n;i++)h[1][i][a[1]][g[i+1][0][1][a[1]]]=1;
	dfs(1);
	printf("%d\n",res);
	return 0;
}

XXXIV.CF848D Shake It!

首先我们考虑一个流程。我们有最开始的一条边;接着,我们可以以这条边为轴心,加入若干组边,每加入一组边流量都会加一。

然后,我们可以选择一组边 sot,往上面继续加边。这样加边的贡献,是 so 的流量与 ot 的流量,二者的 min

于是我们就构思了一个状态:fi,j 表示往一条边上操作 i 次,产生 j 点流量的方案数。

考虑转移。

首先我们可以执行基础操作,即以当前边为轴心进行一次操作。每执行一次,流量就会加一。

然后我们可以选择一条边,执行加边操作,其可以由 fi1,j1fi2,j2 组合,产生 fi1+i2,min(j1,j2) 的流量。

于是我们可以令一个 gi,j 的辅助数组,表明对于以当前边为轴心的一次操作以及其上的额外若干次操作,共使用 i 次操作、产生 j 点流量的方案数。

考虑由 g 拼成 f。这是一个 i,j 两维同时的背包问题;但是问题就在于,我们要 判同构

考虑 gi,j 这一个东西。一个 (i,j) 的贡献可以写成 xiyj;它重复若干次的结果是 11xiyj;一共 gi,j 种不同的颜色,因此总共 1(1xiyj)gi,j

那么手动做半在线二维卷积和半在线求逆,问题就在四方的复杂度内解决了。


但是这个式子听上去太扯了。我们考虑优化。

首先注意到不同的 gi,j 间不会触发重复计算。于是我们算完一个 gi,j 就把它背包进去用乘法原理合并,是正确的;

然后考虑 gi,j 的背包:假如我们选择 i 个来自 gi,j 的东西,则方案数事实上是 (i+gi,j1gi,j1) 这个 隔板法 的形式!

注意到其等价形式 1(1xiyj)gi,j 中的 g可以对模数取模的(来自多项式取模的结论),而上述组合数事实上是可以手动乘 i 项解决的。于是我们可以简单得出选 i 个时的系数。复杂度五方是简单的,四方带对数的复杂度也可以根据调和级数分析出来。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int n,m,f[60][60],g[60][60],h[60][60],fac[60],inv[60];
int main(){
	scanf("%d%d",&n,&m);
	fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
	inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
	f[0][0]=1;
	for(int i=0;i<=n;i++){
		for(int j=0;j<=i;j++){
			// printf("(%d,%d):%d\n",i,j,f[i][j]);
			for(int I=0;i+I<=n&&I<=i;I++)for(int J=0;J<=(I==i?j:I);J++)
				(g[i+I][min(j,J)]+=(i==I&&j==J?1ll:2ll)*f[i][j]*f[I][J]%mod)%=mod;
		}
		for(int j=0;j<=i;j++){
			// printf("[%d,%d]:%d\n",i,j,g[i][j]);
			for(int k=0,pro=1;k*(i+1)<=n;pro=1ll*pro*(g[i][j]+k)%mod,k++)
				for(int I=0;I+k*(i+1)<=n;I++)for(int J=0;J<=I;J++)
					// printf("%d:(%d,%d)<%d>%d,%d(%d,%d)\n",k,I,J,f[I][J],pro,inv[k],I+k*(i+1),J+k*(j+1)),
					(h[I+k*(i+1)][J+k*(j+1)]+=1ll*pro*inv[k]%mod*f[I][J]%mod)%=mod;
			for(int I=0;I<=n;I++)for(int J=0;J<=I;J++)f[I][J]=h[I][J],h[I][J]=0;
		}
	}
	printf("%d\n",f[n][m-1]);
	return 0;
}

XXXV.唯一匹配

在一张 n×m 方格图上,有若干格子被涂色了。两个同行或同列的格子可以匹配。一个局面是好的,当且仅当存在且仅存在一组完美匹配。

新增尽量少的涂色格子,使得局面合法,或者报告不可能。构造一种方案。

数据范围:n,m1000

Observation 1.把网格图中染色格子抽象成点,并在同行或同列的点间连边。若得到的新图中存在环,则局面必然不合法。

首先如果存在环则必然存在偶环:因为我们可以让环上的相邻两条边,一条连接同行的点,一条连接同列的点。假如环上存在连续若干个同行或同列点,那只保留第一个和最后一个必然仍成环。因为相邻两条边方向不同,所以这必然是偶环。

考虑反证。假定存在一组完美匹配,下证必然存在另一种与之不同的完美匹配。

假如找到的完美匹配中,偶环上的点没有到偶环外点的匹配,则把环上边状态反转必然仍得到一组完美匹配。

否则,环上有至少一个点向外有匹配。而因为环是偶环且存在完美匹配,所以向外有匹配的点数必然是偶数。这些向外有匹配的点将环划分成若干段,每段长度都是偶数(因为每段都完全由环上边匹配构成)

注意到一个环外点如果与环上点有边,则其必然和与该环上点相邻的另一个点有边,因为与某个环上点相邻的两个点必然一个与之同行、一个与之同列,故其中必然有一个与该环外点同行或同列。

于是我们把所有环外点同时调整成其另一个匹配点。这必然不会产生冲突,因为每段中的点数都是偶数;并且调整后每段中点数仍是偶数,这就意味着仍然可以得到一组完美匹配。

综上,这张图中不能成环。但是,全部点都在同一行或同一列的环不在此列,因为它们删点后最终得到了两个点,不成环

这时我们就可以祭出一个套路:把行编号作为二分图左部,列编号作为二分图右部,一个被染色的格子看作是连接其对应行列对应点间的边。则,一个在前一个图中的非同行同列环在新图中仍是环,反之亦然。

而这表明,新图是森林。

考虑新图上的一对匹配,则其对应了新图上的一对有公共端点的边。新图上的一组完美匹配就是为每条边都分配到某对元素中。完美匹配非唯一,当且仅当存在两对边,其公共端点相同。这显然是必要的,因为否则我们可以交叉互换两对边中的元素得到一组新解;这又是充分的,因为其事实上是暴力流程(每次将一条仅与一条边有公共端点的边配对)得到的结果。

于是我们可以构思一个算法:fi 表示节点 i 的子树中至少需要引入多少游离点,才能把子树中的边全都配到某个完美匹配中且不存在相同的公共端点。

然后我们紧接着发现,左部的游离点和右部的游离点的效用是不同的,我们不能简单统一它们。因此我们需要同时记录状态中使用的左右部游离点数。但是发现在其中一个固定的时候另一个越少越好,于是我们只需记 fi,j 表示用 j 个左部游离点时需要最少的右部游离点。j 因为不超过子树大小所以直接树上背包就能做到平方。需要记录 0/1/2 表示树根 没有待匹配的边/有一条待匹配的边/是某个匹配的公共端点,合并是简单的。

现在考虑树间的合并。一棵树的状态可以用 (x,y) 表示,意为其消耗了 x 个左部点和 y 个右部点。不同树的状态仍可以背包合并,得到共计消耗 (X,Y) 的状态。因为显然 X 固定时 Y 愈小愈好,所以真正有效的 (X,Y) 组只有线性个。

考虑如何验证某个 X,Y 是否合法。我们首先可以用孤立点来满足之。设左、右部的孤立点分别有 a,b 个,则尽量满足后就得到了 max(Xa,0) 以及 max(Yb,0) 的需求。

现在不存在孤立点。则每个连通块中必然存在 至少一个左部点 以及 至少一个右部点。这意味着若共有 C 个非孤立点的连通块,则在连通块间连边至多可以额外满足 C1 个游离点的需求,不论左右。故若 max(Xa,0)+max(Yb,0)<C 则需求可以被满足,否则无法被满足。答案就是全体合法 X+Y 的最小值。构造方案是平凡的。

需要特判 C=0 的场合。此时相当于整张图中没有任何染色格子,因此直接输出 0 即可。

代码:

#include<bits/stdc++.h>
using namespace std;
void chmn(int&x,int y){if(x>y)x=y;}
int n,m;
vector<int>v[2010];
char s[1010][1010];
bool vis[2010];
void dfs(int x,int fa){
	vis[x]=true;
	for(auto y:v[x])if(y!=fa){
		if(vis[y]){puts("-1");exit(0);}
		dfs(y,x);
	}
}
int f[2010][2010][3],g[2010][3],sz[2010];
int F[2010][2010],FR[2010][2010],A,B,C,res=0x3f3f3f3f,rp=-1;
int FF[2010][2010][3],RR[2010][2010][3];
void DP(int x,int fa){
	vis[x]=false;
	f[x][0][0]=0;if(x<=n)f[x][0][1]=1;else f[x][1][1]=0;sz[x]=1;
	for(auto y:v[x])if(y!=fa){
		DP(y,x);
		for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y];j++){
			chmn(g[i+j][1],f[x][i][0]+f[y][j][0]);
			chmn(g[i+j][0],f[x][i][0]+f[y][j][1]);
			chmn(g[i+j][1],f[x][i][0]+f[y][j][2]);
			chmn(g[i+j][2],f[x][i][1]+f[y][j][0]);
			chmn(g[i+j][1],f[x][i][1]+f[y][j][1]);
			chmn(g[i+j][2],f[x][i][1]+f[y][j][2]);
			// chmn(...,f[x][i][2]+f[y][j][0]);
			chmn(g[i+j][2],f[x][i][2]+f[y][j][1]);
			// chmn(g[i+j][1],f[x][i][2]+f[y][j][2]);
		}
		sz[x]+=sz[y];
		for(int i=0;i<=sz[x];i++)for(int j=0;j<=2;j++)f[x][i][j]=g[i][j],g[i][j]=0x3f3f3f3f;
	}
	// printf("%d:",x);
	// for(int i=0;i<=sz[x];i++)printf("[%d,%d,%d]",f[x][i][0],f[x][i][1],f[x][i][2]);puts("");
}
int TTJ[2010],TTK[2010];
void CHMN(int&x,int&y,int z,int w){if(x>z)x=z,y=w;}
bool nd[2010];
void PD(int x,int fa,int TJ,int TK){
	int nm=0,sm=0;
	FF[0][0][0]=0;if(x<=n)FF[0][0][1]=1;else FF[0][1][1]=0;sz[x]=1;
	for(auto y:v[x])if(y!=fa){
		for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y];j++){
			CHMN(FF[nm+1][i+j][1],RR[nm+1][i+j][1],FF[nm][i][0]+f[y][j][0],j);
			CHMN(FF[nm+1][i+j][0],RR[nm+1][i+j][0],FF[nm][i][0]+f[y][j][1],j);
			CHMN(FF[nm+1][i+j][1],RR[nm+1][i+j][1],FF[nm][i][0]+f[y][j][2],j);
			CHMN(FF[nm+1][i+j][2],RR[nm+1][i+j][2],FF[nm][i][1]+f[y][j][0],j);
			CHMN(FF[nm+1][i+j][1],RR[nm+1][i+j][1],FF[nm][i][1]+f[y][j][1],j);
			CHMN(FF[nm+1][i+j][2],RR[nm+1][i+j][2],FF[nm][i][1]+f[y][j][2],j);
			CHMN(FF[nm+1][i+j][2],RR[nm+1][i+j][2],FF[nm][i][2]+f[y][j][1],j);
		}
		sz[x]+=sz[y],nm++;
	}
	reverse(v[x].begin(),v[x].end());
	for(auto y:v[x])if(y!=fa){
		int j=RR[nm][TJ][TK],i=TJ-j;
		TTJ[y]=j;
		if(TK==0)TTK[y]=1,TK=0;
		else if(TK==1){
			if(FF[nm-1][i][0]+f[y][j][0]==FF[nm][TJ][TK])TTK[y]=0,TK=0;
			else if(FF[nm-1][i][0]+f[y][j][2]==FF[nm][TJ][TK])TTK[y]=2,TK=0;
			else TTK[y]=1,TK=1;
		}else{
			if(FF[nm-1][i][1]+f[y][j][0]==FF[nm][TJ][TK])TTK[y]=0,TK=1;
			else if(FF[nm-1][i][1]+f[y][j][2]==FF[nm][TJ][TK])TTK[y]=2,TK=1;
			else TTK[y]=1,TK=2;
		}
		for(int k=0;k<=sz[x];k++)for(int K=0;K<=2;K++)FF[nm][k][K]=0x3f3f3f3f,RR[nm][k][K]=0;
		TJ=i;
		sz[x]-=sz[y],nm--;
	}
	for(int k=0;k<=sz[x];k++)for(int K=0;K<=2;K++)FF[nm][k][K]=0x3f3f3f3f,RR[nm][k][K]=0;
	if(TK)nd[x]=true;
	for(auto y:v[x])if(y!=fa)PD(y,x,TTJ[y],TTK[y]);
}
bool rt[2010];
int sta[2010],id[2010];
vector<int>L,R;
int dsu[2010];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
void merge(int x,int y){x=find(x),y=find(y);if(x!=y)dsu[y]=x;}
int main(){
	freopen("match.in","r",stdin);
	freopen("match.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%s",s[i]+1);
		for(int j=1;j<=m;j++)if(s[i][j]=='#')v[i].push_back(j+n),v[j+n].push_back(i);
	}
	for(int i=1;i<=n+m;i++)if(!vis[i])dfs(i,0);
	memset(f,0x3f,sizeof(f)),memset(g,0x3f,sizeof(g));
	memset(F,0x3f,sizeof(F));
	int nm=0,sm=0;F[0][0]=0;
	for(int i=1;i<=n+m;i++)if(vis[i]){
		rt[i]=true,DP(i,0);
		for(int j=0;j<=sm;j++)for(int k=0;k<=sz[i];k++)
			if(F[nm][j]+min(f[i][k][0],f[i][k][2])<F[nm+1][j+k])
				F[nm+1][j+k]=F[nm][j]+min(f[i][k][0],f[i][k][2]),
				FR[nm+1][j+k]=k;
		if(sz[i]==1){
			if(i<=n)L.push_back(i),A++;
			else R.push_back(i),B++;
		}else C++;
		id[++nm]=i,sm+=sz[i];
	}
	if(!C){puts("0");for(int i=1;i<=n;i++)printf("%s\n",s[i]+1);return 0;}
	for(int j=0;j<=sm;j++){
		int X=j,Y=F[nm][j],Z=X+Y;
		X=max(X-A,0),Y=max(Y-B,0);
		if(X+Y<C&&res>Z)res=Z,rp=j;
	}
	if(rp==-1){puts("-1");return 0;}
	printf("%d\n",res);
	for(int i=nm,j=rp;i;i--)sta[id[i]]=FR[i][j],j-=FR[i][j];
	memset(FF,0x3f,sizeof(FF));
	for(int i=1;i<=n+m;i++)if(rt[i])PD(i,0,sta[i],f[i][sta[i]][0]<f[i][sta[i]][2]?0:2);
	for(int i=1;i<=n+m;i++)dsu[i]=i;
	for(int i=1;i<=n+m;i++)for(auto j:v[i])merge(i,j);
	for(int i=1;i<=n+m;i++)if(nd[i]){
		if(i<=n&&!R.empty())s[i][R.back()-n]='#',merge(i,R.back()),R.pop_back(),nd[i]=false;
		if(i>n&&!L.empty())s[L.back()][i-n]='#',merge(L.back(),i),L.pop_back(),nd[i]=false;
	}
	for(int i=1;i<=n;i++)if(nd[i])for(int j=n+1;j<=n+m;j++)
		if(find(i)!=find(j)){merge(i,j),nd[i]=false,s[i][j-n]='#';break;}
	for(int j=n+1;j<=n+m;j++)if(nd[j])for(int i=1;i<=n;i++)
		if(find(i)!=find(j)){merge(i,j),nd[j]=false,s[i][j-n]='#';break;}
	for(int i=1;i<=n;i++)printf("%s\n",s[i]+1);
	return 0;
}

XXXVI.[LOJ#3404][2020-2021 集训队作业]Defend City

首先第一个观察是每个方向必然选择至少一个塔,不然四个角无法被覆盖。

然后第二个观察是,要么 1,3 方向各自只选一个,要么 2,4 方向各自只选一个。

Proof: 首先,在解法中,不可能 1,3 方向的范围两两无交且 2,4 方向的范围两两无交(交集存在但面积为零亦可行)。

不妨假设 1,3 方向上存在两个范围有交。则因为有交,所以画出图来就能发现,对于任意有交的 1,3 方案,未被覆盖的部分是左上角的一个矩形以及右下角的一个矩形,且二者无交。

这两个矩形必然要被 2,4 象限的矩形覆盖。但是我们发现,这两个矩形各自都 不能 被若干个 各自无法完全覆盖之 的矩形的 并集 完全覆盖。这意味着,其合法当且仅当存在一个 2 象限矩形完全覆盖左上角矩形,且存在一个 4 象限矩形完全覆盖右下角矩形。

那么显然如果存在,则各自选择一个矩形即可。同理,若 2,4 方向范围有交,则 1,3 方向矩形只需各自选择一个。

不妨先考虑 1,3 方向有交的情形。2,4 有交可以反转矩形并取 min

考虑钦定一个 2 方向的矩形。则,1 方向首先要选择一个 左边界不右于 2 方向矩形右边界 的矩形,同理 3 方向要选择一个 上边界不下于 2 方向矩形下边界 的矩形。显然前者要选择最下矩形,后者要选择最右矩形。需要注意的是,我们必须保证该最下和最右矩形有交。这可以通过钦定 2 方向矩形的右边界,然后找到此时下边界最小的 2 方向矩形,以及下边界最小且与之有交的 1 方向矩形。假如该 1 方向矩形的下边界在 2 方向矩形下边界以上,则可能无交,要手动把 2 方向矩形的下边界提到与 1 方向矩形下边界平齐的位置(显然,这样提升必然是提升尽量小的距离,因为提升越小越有可能找到符合条件的 3 方向矩形)。然后找到此时右边界最右且与 2 方向矩形有交的 3 方向矩形。假如该 3 方向矩形的右边界在我们钦定的 2 方向矩形以左,则其不可能有交,手动跳过。否则我们得到了一个态,其仅有某个 1 方向矩形下边界以下以及某个 3 方向矩形右边界以右的部分未被覆盖。

这个态可以用一个二元组 (x,y) 描述,其中 x1 方向选择的最下矩形,y3 方向选择的最右矩形。(x,y) 意味着没有被覆盖的部分为 x 矩形的下边界以下部分及 y 矩形的右边界以右部分。

考虑 (x,y) 可以转移的态:我们有三种转移:

  • 判定是否存在一个完美覆盖 x 矩形的下边界以下部分及 y 矩形的右边界以右部分的 4 方向矩形。假如存在这样的矩形,就可以直接用一个矩形覆盖未被覆盖的部分,故可直接转移至答案。
  • 找到 左边界不右于 y 右边界 的最下矩形 z,转移到 (z,y)
  • 找到 上边界不下于 x 下边界 的最右矩形 z,转移到 (x,z)

注意到第一种转移成立时,二三种转移显然都不会执行;而二三种转移只会执行一个,因为其中的一种转移会转移到其自身。(这是因为我们调用的所有 (x,y) 都满足 x 是左边界不右于 y 右边界的最下矩形,或 y 是上边界不下于 x 下边界的最右矩形。这进一步直接得出,本质不同的 (x,y) 状态只有线性个。)

于是我们直接大力 DP,复杂度就是线性的。注意到 (x,y) 的初态可能不满足上述性质,故初态要同时尝试两种转移。

对于所有本质不同的 (x,y),转移顺序要正确,可以使用双针来保证顺序。

时间复杂度线性。

代码:

const int N=1001000;
void chmx(int&x,int y){if(x<y)x=y;}
void chmn(int&x,int y){if(x>y)x=y;}
int n,ext[5][N],eyt[5][N],X[N],Y[N],D[N];
int f[N],g[N];
int amin(){
	// puts("AMIN!!");
	for(int i=0;i<=n+1;i++)
		ext[1][i]=ext[2][i]=eyt[1][i]=eyt[4][i]=n+2,
		ext[3][i]=ext[4][i]=eyt[2][i]=eyt[3][i]=-1;
	// for(int i=1;i<=n;i++)printf("[%d,%d,%d]",X[i],Y[i],D[i]);puts("");
	for(int i=1;i<=n;i++)
		(D[i]==1||D[i]==2?chmn:chmx)(ext[D[i]][X[i]],Y[i]),
		(D[i]==1||D[i]==4?chmn:chmx)(eyt[D[i]][Y[i]],X[i]);
	for(int i=1;i<=n+1;i++)
		chmn(ext[1][i],ext[1][i-1]),chmx(ext[4][i],ext[4][i-1]),
		chmn(eyt[1][i],eyt[1][i-1]),chmx(eyt[2][i],eyt[2][i-1]);
	for(int i=n;i>=0;i--)
		chmn(ext[2][i],ext[2][i+1]),chmx(ext[3][i],ext[3][i+1]),
		chmn(eyt[4][i],eyt[4][i+1]),chmx(eyt[3][i],eyt[3][i+1]);
	for(int i=0;i<=n+1;i++)f[i]=g[i]=0x3f3f3f3f;
	int ret=0x3f3f3f3f;
	for(int i=0;i<=n+1;i++){
		if(ext[2][i]>n+1||ext[1][i]>n+1)continue;
		int z=max(ext[1][i],ext[2][i]);
		int x=ext[1][i],y=eyt[3][z];
		// printf("<%d,%d>|(%d,%d)\n",i,ext[2][i],x,y);
		if(y<i)continue;
		if(ext[4][y]>=x||eyt[4][x]<=y)return 4;
		if(eyt[3][x]==y)chmn(f[x],3);else if(eyt[3][x]>y)chmn(f[x],4);
		if(ext[1][y]==x)chmn(g[y],3);else if(ext[1][y]<x)chmn(g[y],4);
	}
	for(int i=n+1,j=0;;i--){
		auto transJ=[&](){
			if(g[j]!=0x3f3f3f3f){
				int x=ext[1][j],y=j;
				// printf("(%d,%d):%d\n",x,y,g[j]);
				if(ext[4][y]>=x||eyt[4][x]<=y)chmn(ret,g[j]+1);
				if(eyt[3][x]==y)chmn(f[x],g[j]);else if(eyt[3][x]>y)chmn(f[x],g[j]+1);
			}
		};
		auto transI=[&](){
			if(f[i]!=0x3f3f3f3f){
				int x=i,y=eyt[3][i];
				// printf("[%d,%d]:%d\n",x,y,f[i]);
				if(ext[4][y]>=x||eyt[4][x]<=y)chmn(ret,f[i]+1);
				if(ext[1][y]==x)chmn(g[y],f[i]);else if(ext[1][y]<x)chmn(g[y],f[i]+1);
			}
		};
		if(i<0){for(;j<=n+1;j++)transJ();break;}
		for(;j<=n+1&&ext[1][j]>i||ext[1][j]==i&&j<eyt[3][i];j++)transJ();
		transI();
	}
	// printf("%d\n",ret);
	return ret;
}
void mina(){
	read(n);
	for(int i=1;i<=n;i++)read(X[i]),read(Y[i]),read(D[i]);
	int ret=amin();
	for(int i=1;i<=n;i++)Y[i]=n-Y[i]+1,D[i]=5-D[i];
	ret=min(ret,amin());
	if(ret==0x3f3f3f3f)puts("Impossible");
	else printf("%d\n",ret);
}
int main(){
	freopen("defend.in","r",stdin);
	freopen("defend.out","w",stdout);
	int T;read(T);
	for(int i=1;i<=T;i++)printf("Case #%d: ",i),mina();
	return 0;
}

XXXVII.火之神神乐

数轴上从左往右有若干等间距的箭头,其中左箭头会向左移动,右箭头会向右移动。所有箭头移动速度均相同。

当两个箭头相遇时,有 P 的概率左箭头留下、右箭头消失,否则左箭头消失、右箭头留下。

i 个箭头有 pi 的概率为右箭头,否则为左箭头。

求最终恰剩余 A 个右箭头和 B 个左箭头的概率。

数据范围:n5000。答案对 109+7 取模。

首先一个显然的想法是令 fi,b,a 表示长度为 i 的前缀中,剩余 b 个左箭头和 a 个右箭头的方案数。考虑转移:在加入右箭头时会有 a 增加一,在加入左箭头时会令 a 减小若干,且若减少到零就令 b 增加。

然后发现,当且仅当 a 为零时,b 才会增加。但是我们这样做并不能良好地利用这个性质。

怎么办呢?我们发现这与一类 同时要记录前缀和以及前缀 max 的问题是类似的,而这种东西的套路往往是改变 DP 顺序,倒序 DP,这样就能少记一维前缀 max

那么回到本题,我们考虑改变 DP 顺序,令 fi,j 表示长度为 i 的前缀中有 j 个左箭头的方案数。转移如果来一个左箭头就有 j 增加 1,来一个右箭头就有 j 减小若干,要保证 j 时刻为正,初态为 fi,1=1pi,其中 i 为钦定的最后一个幸存的左箭头,终态为 f0,b。右箭头对应的后缀亦可简单处理。

单次 DP 复杂度 O(n3)。把幂次拆开然后用前缀和简单优化一下即可做到 O(n2)

对于每个 i 分开 DP 一下,复杂度 O(n3)。把 DP 顺序倒一下,从 f0,bfi,1,复杂度就做到平方。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
const int inv100=ksm(100);
int n,P,Q,A,B,p[5010],res;//P:the probability of a leftist winning a rightist; Q:the opposite.
int Pov[5010],Pvo[5010];
int Qov[5010],Qvo[5010];
int f[5010][5010],g[5010][5010],s[5010];
int F[5010],G[5010];
//f:remaining a leftist
//g:remaining a rightist
int main(){
	freopen("fire.in","r",stdin);
	freopen("fire.out","w",stdout);
	scanf("%d%d%d",&n,&Q,&P),P=1ll*P*ksm(P+Q)%mod,Q=(1+mod-P)%mod;
	Pov[0]=Qov[0]=1;for(int i=1;i<=n;i++)Pov[i]=1ll*Pov[i-1]*P%mod,Qov[i]=1ll*Qov[i-1]*Q%mod;
	for(int i=0;i<=n;i++)Pvo[i]=ksm(Pov[i]),Qvo[i]=ksm(Qov[i]);
	for(int i=1;i<=n;i++)scanf("%d",&p[i]),p[i]=1ll*p[i]*inv100%mod;
	scanf("%d%d",&A,&B);
	f[0][B]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)s[j]=(1ll*f[i-1][j]*Pvo[j]+s[j-1])%mod;
		for(int j=1;j<=n;j++){
			f[i][j]=(1ll*f[i-1][j+1]*(1+mod-p[i])+f[i][j])%mod;
			if(!P)f[i][j]=(1ll*f[i-1][j]*p[i]%mod*Q+f[i][j])%mod;
			else f[i][j]=(1ll*s[j]*p[i]%mod*Q%mod*Pov[j]+f[i][j])%mod;
		}
	}
	F[0]=(B==0);for(int i=1;i<=n;i++)F[i]=1ll*f[i-1][1]*(1+mod-p[i])%mod;

	g[n+1][A]=1;
	for(int i=n;i;i--){
		for(int j=1;j<=n;j++)s[j]=(1ll*g[i+1][j]*Qvo[j]+s[j-1])%mod;
		for(int j=1;j<=n;j++){
			g[i][j]=(1ll*g[i+1][j+1]*p[i]+g[i][j])%mod;
			if(!Q)g[i][j]=(1ll*g[i+1][j]*(1+mod-p[i])%mod*P+g[i][j])%mod;
			else g[i][j]=(1ll*s[j]*(1+mod-p[i])%mod*P%mod*Qov[j]+g[i][j])%mod;
		}
	}
	G[n+1]=(A==0);for(int i=1;i<=n;i++)G[i]=1ll*g[i+1][1]*p[i]%mod;

	// for(int i=0;i<=n;i++)printf("%d ",F[i]);puts("");
	// for(int i=0;i<=n;i++)printf("%d ",G[i+1]);puts("");

	for(int i=0;i<=n;i++)res=(1ll*F[i]*G[i+1]+res)%mod;
	printf("%d\n",res);
	return 0;
}

XXXVIII.冰山

一张 01 矩阵,矩阵外的部分看作 2。任意时刻,若一个 0:

  • 左侧和右侧都非 2。
  • 上侧和下侧都非 2。

上述两条件 均不满足 时,这个 0 会变成 2。1 永远不会改变

给定矩阵中的一个位置 (X,Y),求至少要把多少个 1 改成 0,才能在充分多的时间后,有 (X,Y) 为 2。

数据范围:n,m40

首先我们要尝试模拟一张 01 矩阵在经过充分多的时间后的状态。

最浅显的想法显然是用 bfs 每次翻一个 2。但是显然 bfs 并非能够很好用数学语言描述的流程。

怎么办呢?另一个很浅显的想法是,求出 1 的部分在四个方向上的单调栈,则单调栈内部夹着的部分即为最终会留下的部分。

但是问题在于,当边界有交的时候,夹着的部分就不会是简单图形,其事实上会分裂成两个或者多个部分,分开继续处理。

我们考虑如果要让 (X,Y) 变成 2,可以怎么做:

  • 将其左上、左下、右上、右下四个方向之一的所有 1 全都删去,这样 (X,Y) 就会直接不被包含。代价是该方向的 1 的数量。
  • 让边界分裂。这需要选择一个点,将其左下、右上的全部 1 删去,然后若 (X,Y) 在其左上则递归入左上矩形处理,否则在右下则递归入右下矩形处理。同理,亦可删去其左上、右下的全部 1,递归入左下或右上处理。

我们可以用一个 (d,u,l,r) 四元组描述一个态。第二种操作可以枚举其中每个点进行转移;第一种操作枚举所有包含 (X,Y) 的态求结果的 min 即可。某个子矩形中 1 的数量可以用二维前缀和简单处理。复杂度 O(n6)

代码:

#include<bits/stdc++.h>
using namespace std;
int res,n,m,f[50][50][50][50],s[50][50],a[50][50],X,Y;
int SUM(int d,int u,int l,int r){return s[u][r]-s[d-1][r]-s[u][l-1]+s[d-1][l-1];}
void chmn(int&x,int y){if(x>y)x=y;}
int main(){
	freopen("iceberg.in","r",stdin);
	freopen("iceberg.out","w",stdout);
	scanf("%d%d%d",&res,&n,&m),res=0x3f3f3f3f;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){scanf("%d",&a[i][j]);if(a[i][j]==2)X=i,Y=j,a[i][j]=0;}
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)s[i][j]=a[i][j]+s[i][j-1]+s[i-1][j]-s[i-1][j-1];
	memset(f,0x3f,sizeof(f)),f[1][n][1][m]=0;
	for(int d=1;d<=n;d++)for(int u=n;u>=d;u--)for(int l=1;l<=m;l++)for(int r=m;r>=l;r--){
		for(int p=d;p<=u;p++)for(int q=l;q<=r;q++)
			chmn(f[d][p][l][q],SUM(p+1,u,l,q)+SUM(d,p,q+1,r)+f[d][u][l][r]),
			chmn(f[p][u][l][q],SUM(d,p-1,l,q)+SUM(p,u,q+1,r)+f[d][u][l][r]),
			chmn(f[d][p][q][r],SUM(p+1,u,q,r)+SUM(d,p,l,q-1)+f[d][u][l][r]),
			chmn(f[p][u][q][r],SUM(d,p-1,q,r)+SUM(p,u,l,q-1)+f[d][u][l][r]);
		// printf("<%d,%d,%d,%d>:%d\n",d,u,l,r,f[d][u][l][r]);
		if(d<=X&&X<=u&&l<=Y&&Y<=r)
			chmn(res,f[d][u][l][r]+SUM(X,u,Y,r)),
			chmn(res,f[d][u][l][r]+SUM(X,u,l,Y)),
			chmn(res,f[d][u][l][r]+SUM(d,X,Y,r)),
			chmn(res,f[d][u][l][r]+SUM(d,X,l,Y));
	}
	printf("%d\n",res);
	return 0;
}

XXXIX.[HDU7197]Multiply 2 Divide 2

首先一个观察是我们只需记录一个 ai 被除了多少次又乘了多少次即可。除的次数是 log 级别的;乘的次数是……

不知道。事实上,通过构造可以达到 200 次之巨。

然后就是 fi,j,k 表示 ai 除了 j 次乘了 k 次。但这玩意常数太大过不去。

若任意时刻 k>logV,则考虑 fi,j,k+1 时,转移到终态时的最优解。事实上,接下来的部分必然亦会全体乘二,因为若这样不最优则全体除二会得到不乘二时的更优解。

那么设一个 gi,j 表示除 j 次乘到大于 V 后,后缀最优方案。倒着转移即可。

时间复杂度 O(nlog2V)

代码:

#include<bits/stdc++.h>
using namespace std;
const int V=100000;
const int LG=17;
int n,a[100100],f[100100][20][20],g[100100][20],ps[100100][20],res;
void chmn(int&x,int y){if(x>y)x=y;}
void mina(){
	scanf("%d",&n),res=0x3f3f3f3f;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)for(int j=0;a[i]>>j;j++)for(ps[i][j]=0;((a[i]>>j)<<ps[i][j])<=V;ps[i][j]++);
	memset(f,0x3f,sizeof(f));
	for(int j=0;a[1]>>j;j++)for(int k=0;k<=ps[1][j];k++)f[1][j][k]=j+k;
	for(int i=1;i<n;i++){
		vector<pair<int,int> >u,v;
		for(int d=-LG;d<=LG;d++)for(int j=LG;j>=0&&j+d>=0;j--){
			int k=j+d;
			if((a[i]>>j)&&k<=ps[i][j])u.emplace_back(j,k);
			if((a[i+1]>>j)&&k<=ps[i+1][j])v.emplace_back(j,k);
		}
		for(int j=0,k=0,mn=0x3f3f3f3f;j<v.size();j++){
			for(;k<u.size();k++){
				int x=(a[i]>>u[k].first)<<u[k].second;
				int y=(a[i+1]>>v[j].first)<<v[j].second;
				if(x>y)break;
				chmn(mn,f[i][u[k].first][u[k].second]);
			}
			f[i+1][v[j].first][v[j].second]=mn+v[j].first+v[j].second;
		}
	}
	memset(g,0x3f,sizeof(g));
	for(int j=0;a[n]>>j;j++)g[n][j]=0;
	for(int i=n-1;i;i--)for(int j=0;a[i]>>j;j++)for(int k=0;a[i+1]>>k;k++){
		int val=g[i+1][k]+k+ps[i+1][k];
		int I=(a[i]>>j)<<ps[i][j],K=(a[i+1]>>k)<<ps[i+1][k];
		if(I>K)val+=n-i;
		chmn(g[i][j],val);
	}
	for(int j=0;a[n]>>j;j++)for(int k=0;k<=ps[n][j];k++)chmn(res,f[n][j][k]);
	for(int i=1;i<=n;i++)for(int j=0;a[i]>>j;j++)chmn(res,f[i][j][ps[i][j]]+g[i][j]);
	printf("%d\n",res);
}
int T;
int main(){
	scanf("%d",&T);
	while(T--)mina();
	return 0;
}

XL.大哭

对于 n(ai,bi) 二元组构成的有序序列,记其权值为 nmaxi=1{bi+ij=1ai}。对于一个由若干元组构成的序列,记其权值为任意排列其中元组得到的序列,其最小可能权值。

现在序列中所有元组的 bi 都是确定的,而 ai 取遍 [1,m] 中所有整数。则有共计 mn 种不同的元组序列。

对于取遍 [1,n] 中整数的 K,我们要求出此时:

  • 对于某个序列,将恰 nK 个元素的 ai 赋作零,得到的新序列的最小可能权值。
  • 求出上述权值关于一切序列之和,对 998244353 取模。

数据范围:n30,m20,1bi100

首先可以暴力验证一下,然后发现 bi 递减排列必然是最优的方案。这是比较符合直觉的:越晚选择的元组,其对应的前缀 ij=1ai 就越大,那么就需要一个小的 bi 来平衡其大小。

然后考虑将 nK 个元素的 a 赋成零。我们发现,将被清零的元素直接提到序列首处理必然最优——因为这不会对后缀 a 产生任何影响。故我们发现,清零元素事实上同删去元素是区别很小的:答案仅仅是未被删去元素的结果与 max{bi} 的较大值。

考虑固定 K 然后往外面套一层二分。事实上可以不用二分,直接枚举值然后计算答案大于等于该值的方案数,然后求和即可。

此时我们固定了 K,要计算答案大于等于 V 的方案数。其等于总方案数减去答案小于 V 的方案数。则限制是每个位置要么被删去,要么其前方元素的 a 之和再加上其自身的 b 不超过 V

于是我们设 fi,j,k 表示前 i 个数,选中的数共有 j 个,选中的数的和是 k 的方案数。

这个 DP 可以直接忽略 K 的枚举。

但是问题在于,一组集合可能有多种选择数的方式同时合法。我们需要为每种合法集合选择一种唯一的选数方式,因此我们似乎不太能处理问题。


考虑另一种思路。首先考虑全体 ai 固定的场合,然后因为这玩意长得就很像我们经典的“前缀和的最大值”的问题,于是我们考虑记录后缀信息。

首先考虑选择所有位置时的答案应如何计算。将 b 递增排序,此时新 b 的前缀即为原 b 的后缀。令 fi 表示前 i 项的答案,则有 fi=max(fi1,bi)+ai

然后考虑加入另一维 j 表示选中了多少个数。则有 fi,j=min(fi1,j,max(fi1,j1,bi)+ai)

这似乎是另一种很经典的模型:首先有 i 固定时 fj 增加而增加,故可以维护差分数组 g

归纳可得其是凸的。故我们转移时,首先将一段前缀中的 f 赋成 b(即把前缀中的 g 赋成 0;我们认为 fi,0=bi;则该效果相当于是把前面若干 g 均赋成 0,直到被清零的元素和大于 bibi1,此时把多清零的部分加回去),然后直接插入 ai 即可。

具体而言,我们用小根堆维护差分数组。不断 pop 堆顶直到 pop 的元素和大于 bibi1,然后插入元素和减去 bibi1 再插入 ai 即可。

我们发现,当一个元素被弹出堆后,这意味着该位置上的 DP 值将恰等于 bn。故我们只需维护那些未被弹出堆的元素,则每个元素都意味着一段后缀加。

我们考虑关于每个 a,计算其对答案的贡献。

考虑我们需要的信息:

  • 当前的 a 值。
  • 其之前元素个数。
  • 其之前元素之和。

模拟插入流程即可。

但是问题在于,a 只有在插入的时候才能遵循上述原则;在插入前,我们还需要知道其插入到哪个位置、其之前元素和,才能为 DP 态赋初值。

我们仍然需要模拟流程。考虑钦定 a 插在某个位置。则我们需要知道小于等于其的最大元素和大于其的最小元素,然后插在它们中间。另外开一个 DP 维护之即可,需要同时维护小于等于其的最大元素以及大于其的最小元素。新插入一个 a 时,若其小于最大元素或大于最小元素则可以直接归类,否则分最终目标 a 是在哪侧,进入对应区间即可。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int n,m,b[40],res[40];
//pos;val;num;sum;
int f[34][24][32][1010];
//pos;sml;lar;num;sum.
int g[32][22][22][32][610];
int main(){
	freopen("dk.in","r",stdin);
	freopen("dk.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&b[i]);
	sort(b+1,b+n+1);
	g[0][0][m+1][0][0]=1;
	for(int i=0;i<n;i++){
		for(int l=0;l<=m;l++)for(int r=l;r<=m+1;r++)for(int k=0;k<=i;k++)for(int t=0;t<=k*m;t++)if(g[i][l][r][k][t]){
			int val=g[i][l][r][k][t];
			// printf("G:%d,%d,%d,%d,%d|%d\n",i,l,r,k,t,val);
			int L=l,R=r,T=t;
			T-=b[i+1]-b[i];
			if(T<0)L+=T,T=0;
			if(L<0){
				if(R<=m)R+=L;
				if(R<0)R=0;
				L=0;
			}
			// printf("REALSTA:%d,%d,%d,%d\n",L,R,k,T);
			for(int a=1;a<=m;a++){
				if(a<L){ADD(g[i+1][L][R][k+1][T+a],val);continue;}
				if(a>=R){ADD(g[i+1][L][R][k][T],val);continue;}
				ADD(f[i+1][a][k+1][T+L],val);
				ADD(g[i+1][L][a][k][T],val);
				ADD(g[i+1][a][R][k+1][T+L],val);
			}
		}
		for(int j=1;j<=m;j++)for(int k=0;k<=i;k++)for(int t=0;t<=k*m;t++)if(f[i][j][k][t]){
			int val=f[i][j][k][t];
			// printf("F:%d,%d,%d,%d|%d\n",i,j,k,t,val);
			int T=t,J=j;
			T-=b[i+1]-b[i];
			if(T<0)J+=T,T=0;
			if(J<=0)continue;
			// printf("REALSTA:%d,%d,%d\n",J,k,T);
			for(int a=1;a<=m;a++){
				if(a<J)ADD(f[i+1][J][k+1][T+a],val);
				else ADD(f[i+1][J][k][T],val);
			}
		}
	}
	res[1]=1ll*b[n]*ksm(m,n)%mod;
	for(int j=1;j<=m;j++)for(int k=1;k<=n;k++)for(int t=0;t<=k*m;t++)if(f[n][j][k][t])
		// printf("F:%d,%d,%d,%d|%d\n",n,j,k,t,f[n][j][k][t]),
		ADD(res[k],1ll*j*f[n][j][k][t]%mod);
	for(int k=1;k<=n;k++)ADD(res[k],res[k-1]);
	for(int i=1;i<=n;i++)printf("%d ",res[i]);puts("");
	return 0;
}

XLI.罚抄

给定若干开区间。保证所有开区间的交集至少包含一个整点。你每次可以将一个区间向左或向右平移一单位长度。求最少操作次数,使得区间两两无交。

数据范围:n5000。区间的端点坐标在 int 范围内。

首先终态必然是全体区间首尾紧紧相连。然后考虑某个终态所有区间移动的 Δ,就会发现因为所有区间都包含某个整点所以必然是一段前缀的 Δ 为负,一段后缀的 Δ 为正。

我们显然可以令全体 Δ 同增一或同减一。因为我们的目标是最小化 |Δ|,所以通过调整,在 n 为奇数时,排在中间的那个区间的 Δ 必恰为零;在 n 为偶数时,我们只需保证前一半的 Δ 为负、后一半的 Δ 为正即可;那么令交集中包含的整点为 P,则添加一个额外的区间 (P,P),则我们总是可以保证这个区间的 Δ 为零。

于是我们现在已知,总是恰有一个区间的 Δ 为零。这时又有什么样的表现呢?

我们发现,因为其左侧的全体区间必然都要左移,右侧的全体区间必然都要右移,而贪心分析分析就会发现其左侧区间长度必然从右往左递增,右侧区间长度必然从左往右递增。于是我们可以从大往小分析每个区间,记录当前有多少个区间分到左边,有多少个区间分到右边,然后其代价就能计算得到。DP 是平方的,外层还有再钦定一个不动的区间,复杂度 O(n3)

再分析一下我们的式子。设左右侧的区间标号集合按长度递减顺序排列后分别为 S,T(下标从 0 开始),选的的区间左右端点分别为 P,Q,则该方案的代价可以描述为:

i(rSilSi)×i+(rSiP)+i(rTilTi)×i+(QlTi)=i((rSilSi)×i+rSi)+i((rTilTi)×ilTi)+Q|T|P|S|

然后我们通过盲猜/打表观察/乱证,最终可以得到结论,答案必然在 |S|=|T| 时取得。于是上式可以被写成

i((rSilSi)×i+rSi)+i((rTilTi)×ilTi)+n2(QP)

这个式子的三部分各自都是独立的。于是我们在 DP 中只需额外记一维 0/1 表示 (P,Q) 是否已被确定即可。时间复杂度 O(n2)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
void chmn(ll&x,ll y){if(x>y)x=y;}
int n,P;
pii p[5010];
ll f[5010][5010][2];
int main(){
	freopen("rewrite.in","r",stdin);
	freopen("rewrite.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&p[i].first,&p[i].second),P=max(P,p[i].first+1);
	if(!(n&1))p[++n]=make_pair(P,P);
	sort(p+1,p+n+1,[](const pii&u,const pii&v){return u.second-u.first>v.second-v.first;});
	// for(int i=1;i<=n;i++)printf("<%d %d>\n",p[i].first,p[i].second);
	memset(f,0x3f,sizeof(f));
	f[0][0][0]=0;
	for(int i=1;i<=n;i++)for(int j=0;j<i;j++)for(int k=0;k<2;k++){
		// printf("%d,%d,%d:%lld\n",i-1,j,k,f[i-1][j][k]);
		if(!k)chmn(f[i][j][1],f[i-1][j][k]+1ll*(p[i].second-p[i].first)*(n>>1));
		chmn(f[i][j+1][k],f[i-1][j][k]+(1ll*(p[i].second-p[i].first)*j+p[i].second));
		chmn(f[i][j][k],f[i-1][j][k]+(1ll*(p[i].second-p[i].first)*(i-k-1-j)-p[i].first));
	}
	printf("%lld\n",f[n][n>>1][1]);
	return 0;
}

XLII.[2022互测#13]加速度

在数轴上有一列点 p0,p1,,pn,且 p0=0pi1<pi

有一个点从原点出发,且:

  • 任意时刻,其加速度至多为 mm>0),且无下限、可以为负。
  • 任意时刻,其速度可以突变减少,但是必须非负。
  • 到达 pi 的时刻必须位于 [li,ri] 中。
  • 求到达 pn 的最小时刻,或报告不可能。

数据范围:n5000。其它值均在合理范围内。

首先自己瞎分析分析就能得出,流程必然是:

  • 当前位于 pi
  • 将速度调小若干(或者直接调小到 0 并且等待若干时间;也可以不减小速度,或是减小到零但不等待)
  • 马力全开直接冲到 pi+1

假如不是如上的模式,我们显然可以通过让较靠前的加速度减小、较靠后的加速度增大,来使得平均速率不变的同时、结尾速度更大。而显然,时间和位移均相同时,速度越大越好(因为速度可以随时减小)。不断调整,则除了关键点处要保证时间所以不能再调整以外,在半途中必然是踩满油门的。

一个显然的贪心是,记录当前关键点标号、时间、速度,然后尝试转移到下一个关键点,假如到的不过早就不需要减速,否则需要适当减速。

但是这个东西不一定是正确的。我们可能有另一种策略,是在当前关键点适量减速,然后一路冲过若干关键点,直到下一次减速处。

于是我们只对减速处 DP。设 fi 表示在 i 处进行了减速,然后转移就枚举下一个需要减速的位置是哪里。因为 fi 本身也是需要减速才能到的,所以到达 i 时的时刻必然是 li

直接大力枚举 i、下一个位置,暴力转移需要判定该位置能否可达,复杂度 O(n3);更好的转移是,从当前出发可行的初速度仅有一段区间,因此我们直接维护区间,在过一道关卡时就更新区间、将其缩短即可。

但是我们发现这并不能完美涵盖所有局面:我们是有可能出现 虽然在 i 处没有被 li 拦住,但是我们却需要将速度适当减小以便为下一个 li 做铺垫 的场合。

于是我们修改分析。画平面直角坐标系,x 轴为路程,y 轴为时间,则关键点处的限制其实是要求曲线必须经过某条竖直线段。

则按照我们的分析,两个关键点间必然是按照 m 的加速度运动的。假如发现撞到了 l,就需要适当地将曲线整体上移(指调小初速度)。

假如上移后,我们发现撞到了某个区间的右端点,则显然就不能再上移了。于是我们设中继态应该设为 ri 时的最大速度。

时间复杂度平方。

转移时最好手动分速度减小或主动停留两种情况处理,不要在那里费劲讨论负速度(因为负速度最后还是要手动转化为主动停留)。

代码:

#include<bits/stdc++.h>
using namespace std;
const double eps=1e-6;
int n,m,L[5010],R[5010],p[5010];
double f[5010],res;
int main(){
	// freopen("t4.in","r",stdin);
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;i++)scanf("%d",&p[i]);
	for(int i=0;i<=n;i++)scanf("%d%d",&L[i],&R[i]);
	for(int i=1;i<=n;i++)L[i]=max(L[i],L[i-1]);
	for(int i=n;i;i--)R[i-1]=min(R[i-1],R[i]);
	for(int i=0;i<=n;i++)if(L[i]>R[i]){puts("kaibai");return 0;}
	fill(f,f+n+1,-1),f[0]=0,res=0x3f3f3f3f;
	R[0]=0;
	for(int i=0;i<n;i++)if(f[i]+eps>=0){
		// printf("%d:%lf\n",i,f[i]);
		double l=0,r=f[i];
		for(int j=i+1;j<=n;j++){
			// printf("%d,%d(%lf,%lf)\n",i,j,l,r);
			double ds=p[j]-p[i];
			double dt=R[j]-R[i];
			double V=(ds-0.5*m*dt*dt)/dt;
			// printf("RV:%lf\n",V);
			if(V>=l-eps&&V<=r+eps)f[j]=max(f[j],V+m*dt);
			l=max(l,V);
			dt=L[j]-R[i];
			if(dt-eps>=0){
				V=(ds-0.5*m*dt*dt)/dt;
				// printf("LV:%lf\n",V);
				r=min(r,V);
			}
			if(l-eps>=r)break;
			if(j==n){
				double ext=(-r+sqrt(r*r+2*m*ds))/m;
				res=min(res,R[i]+ext);
			}
		}
		l=R[i],r=0x3f3f3f3f;
		for(int j=i+1;j<=n;j++){
			double ds=p[j]-p[i];
			double T=sqrt((2*ds)/m);
			double S=R[j]-T;
			if(S>=l-eps&&S<=r+eps)f[j]=max(f[j],m*T);
			r=min(r,S);
			S=L[j]-T;
			l=max(l,S);
			if(l-eps>=r)break;
			if(j==n)res=min(res,l+T);
		}
	}
	if(res-eps>=R[n]){puts("kaibai");return 0;}
	printf("%lf\n",res);
	return 0;
}

XLIII.[CF1761F1]Anti-median (Easy Version)

这意味着什么?首先分析长度为三的区间,然后就能发现,中间的数必然小于左右两个数或大于左右两个数。则,必然是奇数位全部是局部最小值、偶数位全部是局部最大值,或者相反。不妨考虑前者。

接下来我们对长度为五的区间分析。考虑奇偶奇偶奇的区间:则中间的那个奇必然不能同时大于左右奇:其要么大于一侧但小于另一侧,要么均小于两侧。考虑不同奇五区间间的联动,就能发现,奇数必然是先减后增,同理偶数必然是先增后减。

是否所有满足该性质的序列都是合法的呢?不妨考虑一个中心为偶的区间且半径为 r(如长度为三的区间半径为一,长度为五的区间半径为二),然后开始讨论。不妨令偶数位的转折点不右于 x,则:

  • 其左侧所有偶数均小于其。其左侧所有奇数,因为小于其相邻的偶数,故亦小于其。这样,已经有 r 个元素必须小于其了。
  • 其右侧首个奇数,依定义亦小于其。
  • 综上,至少有 r+1 个元素小于其,故其不可能是中位数。

转折点在其左侧,以及中心为奇的区间的证明类似。

那么我们既然证明了奇数位全部为局小值、偶数位全部为局大值、奇数先减后增、偶数先增后减是充要条件,下一步就是对这玩意计数了。

我们发现,只要保证两端的奇数位分别低于偶数位,则只要中间的部分分别满足单调性,则整个图形必然满足奇数位全为最小值、偶数位全为最大值。

于是我们把奇数位和偶数位分离,并把奇数位序列与偶数位序列首尾相接,拼成一个环。这个环有什么限制呢?其有两个“极”,分别对应奇数位和偶数位的转折点,然后从一个极到另一个极,环的两半上的元素分别递增。

然后那么就简单了。这是一个类拓扑排序的流程。我们只需设一个状态 fl,r 表示环上的区间 [l,r] 中填入了前 rl+1 小的元素,然后转移枚举 l1rl+2 还是 r+1rl+2 即可。时间复杂度平方。

DP 的时候要注意避免两个极均位于奇数或两个极均位于偶数的场合。这可以通过记录极位于哪一侧解决。同时还要保证所有的位置都是局极值。这可以通过在每个位置被加入时 check 解决。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,p[1010],o[1010],q[1010],m,f[1010][1010],M,res;
bool in(int l,int r,int x){
	if(l<=r)return l<=x&&x<=r;
	return l<=x||x<=r;
}
void mina(){
	scanf("%d",&n),m=0,res=0;
	for(int i=1;i<=n;i++)scanf("%d",&p[i]);
	for(int i=1;i<=n;i+=2)o[++m]=i;M=m,reverse(o+1,o+M+1);
	for(int i=2;i<=n;i+=2)o[++m]=i;
	for(int i=1;i<=n;i++)q[o[i]]=i;
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)f[i][j]=0;
	for(int i=1;i<=M;i++)f[i][i]=(p[o[i]]==-1||p[o[i]]==1);
	// for(int i=1;i<=m;i++)printf("%d ",o[i]);puts("");
	for(int l=1;l<n;l++)for(int i=1,j=i+l-1;i<=n;i++,j++,j=(j>n?j-n:j)){
		// if(f[i][j])printf("(%d,%d):%d\n",i,j,f[i][j]);
		int I=(i==1?n:i-1),J=(j==n?1:j+1);
		if((p[o[I]]==-1||p[o[I]]==l+1)&&
			(o[I]==1||o[I]==n||!(in(i,j,q[o[I]-1])^in(i,j,q[o[I]+1])))){
			if(l+1<n)(f[I][j]+=f[i][j])%=mod;
			else if(I>M)(res+=f[i][j])%=mod;
		}
		if((p[o[J]]==-1||p[o[J]]==l+1)&&
			(o[J]==1||o[J]==n||!(in(i,j,q[o[J]-1])^in(i,j,q[o[J]+1])))){
			if(l+1<n)(f[i][J]+=f[i][j])%=mod;
			else if(J>M)(res+=f[i][j])%=mod;
		}
	}
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)f[i][j]=0;
	for(int i=M+1;i<=m;i++)f[i][i]=(p[o[i]]==-1||p[o[i]]==1);
	for(int l=1;l<n;l++)for(int i=1,j=i+l-1;i<=n;i++,j++,j=(j>n?j-n:j)){
		int I=(i==1?n:i-1),J=(j==n?1:j+1);
		if((p[o[I]]==-1||p[o[I]]==l+1)&&
			(o[I]==1||o[I]==n||!(in(i,j,q[o[I]-1])^in(i,j,q[o[I]+1])))){
			if(l+1<n)(f[I][j]+=f[i][j])%=mod;
			else if(I<=M)(res+=f[i][j])%=mod;
		}
		if((p[o[J]]==-1||p[o[J]]==l+1)&&
			(o[J]==1||o[J]==n||!(in(i,j,q[o[J]-1])^in(i,j,q[o[J]+1])))){
			if(l+1<n)(f[i][J]+=f[i][j])%=mod;
			else if(J<=M)(res+=f[i][j])%=mod;
		}
	}
	printf("%d\n",(1ll*res*(mod+1)>>1)%mod);
}
int T;
int main(){scanf("%d",&T);while(T--)mina();return 0;}

XLIV.小 H 的谜题

给定一个环 a。若你初始能力值为 x,则你可以选择一个 aix 的位置 i,从环上删除其,其两侧元素变成相邻,同时 x 增加 ai。自此之后,你每次可以删除环上的一个元素,但要求:

  • 该元素与上一个被删除的元素相邻。
  • 该元素不超过你的能力值 x

删除后,你的能力值增加 x

求最少的初始能力值 x,使得你可以删完整个环。

数据范围:n2×106。多测,n107

这属于一个经典的类型,即“消灭一个不超过自身能力值的怪物,能力值增加怪物的能力值”的模型。一个经典的结论是,假如碰到一个打不过的怪物,修炼之后打过了,则打过前后能力值至少倍增。这意味着,一共只会碰到对数个这样的需要修炼的怪物。

但是问题在于,本题的复杂度不支持任何对数带到复杂度里面。

怎么办呢?我们不由得想到一个经典的想法:那就是不断往两边探,直到两边都吃不掉,这时再尝试提高基础能力值。

然后我们惊讶地发现,所有这样“需要修炼的区间”,都满足:区间中最大值小于两侧元素。

这不就是笛卡尔树上区间嘛。

但是这是环啊。

我们发现,一旦最大值被吃掉,整个流程就结束了。所以我们可以在最大值处把整个环破开,这样我们每次就只会动一个区间了。

笛卡尔树上的 DP 是简单的。

时间复杂度线性。

代码:

int n,a[2001000];
long long s[2001000];
int f[2001000];
int stk[2001000],tp,ch[2001000][2];
void dfs(int x){
	if(x==n)return;
	int y=ch[x][0],z=ch[x][1];
	dfs(y),dfs(z);
	s[x]=s[y]+a[x]+s[z];
	f[x]=min({1ll*a[x],f[y]+max(a[x]-(s[y]+f[y]),0ll),f[z]+max(a[x]-(s[z]+f[z]),0ll)});
}
void mina(){
	readint(n);
	for(int i=0;i<n;i++)readint(a[i]),ch[i][0]=ch[i][1]=n;
	f[n]=s[n]=0;
	rotate(a,max_element(a,a+n),a+n);
	tp=0;for(int i=0;i<n;i++){
		while(tp&&a[stk[tp]]<a[i])ch[i][0]=stk[tp--];
		if(tp)ch[stk[tp]][1]=i;stk[++tp]=i;
	}
	dfs(stk[1]);
	printf("%d\n",f[stk[1]]);
}

XLV.替换排序

给定数组 ab。你可以执行任意多次,将 a 中的某个元素替换成 b 中的任一元素,然后删去该 b 中元素。

求最少操作次数,使得 a 不降。

数据范围:a,b 长度不超过 5×105保证 b 中所有元素出现次数均不超过 200

显然可以令一个 DP:fi,j 表示强制 ai 不被替换,然后此时其在 [1,i] 的前缀中共消耗了 b 中的 jai 时,前缀中最多保留的元素数。

转移要分类讨论:假如 ai+1 亦保留,那么转移是简单的;否则,也即 fi,j 转移到 fx,y,其中 x>i+1,则:

  • (i,x) 中所有 a 均要被替换。显然我们希望替换至 x1b 越小越好,于是直接从第 j+1ai 开始填入 b 中递增的元素即可。

p 为将 b 递增排序后,首个大于等于 ai 的元素。则填入 ax1 处的元素应为 bp+j+(xi2)

  • bp+j+(xi2)<ax,则转移到 fx,0
  • 否则,若 bp+j+(xi2)=ax,且 q 为首个大于等于 ax 的元素,则转移到 fx,(p+j+(xi2))q+1

第二种转移可以针对每个 p+ji2 的位置开桶维护 f 的最值,这个转移是线性的。剩下就只需要考虑第一种转移即可。

第一种转移可以线段树多一个 log;但是问题是这里有 200n 次单点修改 n 次前缀问 max 因此不太好搞。

考虑摊一下复杂度:修改的实质是对桶中区间 [l,r] 的部分与 fi,max;而我们可以仅用 BIT,将 r 处与 maxfi,max;然后询问 x 处时,在 BIT 中问 x 然后手动访问 x200 个桶(显然,更之前的桶中内容必然已经在 BIT 中考虑过了)。

复杂度 O(nlogn+200n)

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1001000;
void chmx(int&x,int y){if(x<y)x=y;}
int f[2][210],res;
int n,m,a[500100],b[500100];
int B[1001000];
vector<int>v;
int g[2004000];
int t[2004000];
void SET(int x,int y){while(x<=(N<<1))t[x]=max(t[x],y),x+=x&-x;}
int MAX(int x){int ret=0xc0c0c0c0;while(x)ret=max(ret,t[x]),x-=x&-x;return ret;}
int main(){
	freopen("sort.in","r",stdin);
	freopen("sort.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),v.push_back(a[i]);
	for(int i=1;i<=m;i++)scanf("%d",&b[i]),v.push_back(b[i]);
	sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
	for(int i=1;i<=n;i++)a[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1;
	for(int i=1;i<=m;i++)b[i]=lower_bound(v.begin(),v.end(),b[i])-v.begin()+1;
	// for(int i=1;i<=n;i++)printf("%d ",a[i]);puts("");
	// for(int i=1;i<=m;i++)printf("%d ",b[i]);puts("");
	sort(b+1,b+m+1);
	for(int i=1;i<=m;i++)B[b[i]]++;
	memset(f,0xc0,sizeof(f)),f[0][0]=0;
	memset(g,0xc0,sizeof(g));
	memset(t,0xc0,sizeof(t));
	res=(m>=n?0:0xc0c0c0c0);
	for(int i=1;i<=n;i++){
		// printf("[%d]\n",i);
		memset(f[i&1],0xc0,sizeof(f[i&1]));
		int o=lower_bound(b+1,b+m+1,a[i-1])-b;
		int p=lower_bound(b+1,b+m+1,a[i])-b;
		if(a[i-1]==a[i])
			for(int j=0;j<=B[a[i-1]];j++)
				chmx(f[i&1][j],f[!(i&1)][j]+1);
		else if(a[i-1]<a[i])
			for(int j=0;j<=B[a[i-1]];j++)
				chmx(f[i&1][0],f[!(i&1)][j]+1);
		int pos=N+p-i-1;
		chmx(f[i&1][0],MAX(pos));
		for(int j=pos;j>=0&&j>=pos-210;j--)
			chmx(f[i&1][0],g[j]);
		for(int j=1;j<=B[a[i]];j++)
			chmx(f[i&1][j],g[pos+j]);
		// for(int j=0;j<=B[a[i]];j++)
		// 	printf("%d ",f[i&1][j]);puts("");
		for(int j=0;j<=B[a[i]];j++)
			if(m-(p+j)+1>=n-i)
				res=max(res,f[i&1][j]);
		int mx=0xc0c0c0c0;
		for(int j=0;j<=B[a[i-1]];j++)
			chmx(g[N+o+j-(i-1)-2],f[!(i&1)][j]+1),
			mx=max(mx,f[!(i&1)][j]+1);
		SET(N+o+B[a[i-1]]-(i-1)-2,mx);
	}
	printf("%d\n",res<0?-1:n-res);
	return 0;
}

XLVI.[HNOI2019]多边形

我声称,终态必为自 n 连向其它所有点的剖分。

具体而言,考虑若边 (i,n) 不存在,那么考虑其左侧首条边 (j,n) 与右侧首条边 (k,n)(显然必能找到这样的 j,k),那么必然存在连边 (j,k),且在 (j,k) 下方必然可以找到一个 i,满足存在连边 (j,i)(k,i)

那么,j,i,k,n 即为一组可以执行操作的元素,我们可以删去 (j,k) 并连边 (i,n)

显然,对于任何非终态,我们都可以找到某条不存在的 (i,n),进而找到 (i,n) 并执行其,使得 n 出发的边数加一。

有操作数的下界为未与 n 连边的点数:每次操作至多连一条边。而如前述,我们得到了一种每次连一条边的构造,所以下界是可以取到的。那么答案就是未与 n 连边的点数。

我们还要求出方案数。注意到每组相邻的 (j,k) 均会有一个对应的 i,在连上 i 后又会出现 (j,i)(i,k) 两组方案。这是一个二叉树结构:祖先的操作要先于所有后代操作前执行。这是一个树上拓扑序计数问题,按照经典结论,方案数为 size!irootszi

于是我们现在会通过一遍 DP 处理出单次计数。问题是有多次操作。

  • 如果动的操作是 n 多连出一条边,结果是简单的:有一个二叉点裂开了。

  • 如果是 n 少连出一条边,亦是简单的:有两个根的子节点合并了。

  • 否则是子树中一条边出了问题。

    我们发现,其实是如下效果:

        1                    1            
       / \        2         / \        
      2   3   --------->   4   2         
     / \                      / \       
     4 5                      5 3     
    

    实际上是一个类似于 rotate 的过程。

    我们可以发现,rotate 不影响除 2 外其它点的子树大小。那就直接做即可。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int mod=1e9+7;
    int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
    int tp,fac[100100],pro=1;
    vector<int>v[100100];
    int n,m,cnt;
    int fa[100100];
    map<pair<int,int>,int>mp;
    vector<int>u[100100];
    int sz[100100];
    void dfs(int x){sz[x]=1;for(auto y:u[x])if(y!=-1)dfs(y),sz[x]+=sz[y];}
    int solve(int x,int y){
    	if(x+1==y)return -1;
    	int o=mp[make_pair(x,y)]=++cnt;
    	int z=*upper_bound(v[y].begin(),v[y].end(),x);
    	int i=solve(x,z);
    	if(i!=-1)fa[i]=o;u[o].push_back(i);
    	int j=solve(z,y);
    	if(j!=-1)fa[j]=o;u[o].push_back(j);
    	// printf("solve:[%d,%d]=%d\n",x,y,o);
    	return o;
    }
    int main(){
    	scanf("%d",&tp);
    	scanf("%d",&n);
    	fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
    	for(int i=1,x,y;i<=n-3;i++){
    		scanf("%d%d",&x,&y);
    		if(x>y)swap(x,y);
    		v[y].push_back(x);
    	}
    	for(int i=2;i<=n;i++)v[i].push_back(i-1);v[n].push_back(1);
    	for(int i=1;i<=n;i++)sort(v[i].begin(),v[i].end());
    	for(int i=0;i+1<v[n].size();i++){
    		int o=solve(v[n][i],v[n][i+1]);
    		if(o!=-1)fa[o]=0;u[0].push_back(o);
    	}
    	dfs(0);
    	// for(int i=1;i<=cnt;i++)printf("%d ",sz[i]);puts("");
    	// for(int i=1;i<=n;i++)printf("%d ",fac[i]);puts("");
    	for(int i=1;i<=cnt;i++)pro=1ll*pro*sz[i]%mod;
    	if(!tp)printf("%d\n",cnt);
    	else printf("%d %d\n",cnt,1ll*fac[cnt]*ksm(pro)%mod);
    	scanf("%d",&m);
    	for(int i=1,x,y;i<=m;i++){
    		scanf("%d%d",&x,&y);
    		if(x>y)swap(x,y);
    		if(y==n){
    			if(!tp)printf("%d\n",cnt+1);
    			else{
    				int j=lower_bound(v[n].begin(),v[n].end(),x)-v[n].begin();
    				int SZ=1;
    				if(u[0][j-1]!=-1)SZ+=sz[u[0][j-1]];
    				if(u[0][j]!=-1)SZ+=sz[u[0][j]];
    				printf("%d %d\n",cnt+1,1ll*fac[cnt+1]*ksm(1ll*pro*SZ%mod)%mod);
    			}
    			continue;
    		}
    		if(binary_search(v[n].begin(),v[n].end(),x)&&binary_search(v[n].begin(),v[n].end(),y)){
    			if(!tp)printf("%d\n",cnt-1);
    			else{
    				int j=mp[make_pair(x,y)];
    				printf("%d %d\n",cnt-1,1ll*fac[cnt-1]*ksm(pro)%mod*sz[j]%mod);
    			}
    			continue;
    		}
    		if(!tp)printf("%d\n",cnt);
    		else{
    			int j=mp[make_pair(x,y)];
    			assert(fa[j]);
    			int dir=(j==u[fa[j]][1]);
    			int SZ=1;
    			if(u[fa[j]][!dir]!=-1)SZ+=sz[u[fa[j]][!dir]];
    			if(u[j][!dir]!=-1)SZ+=sz[u[j][!dir]];
    			// printf("%d:%d,%d,%d\n",j,fa[j],dir,SZ);
    			printf("%d %d\n",cnt,1ll*fac[cnt]*ksm(1ll*pro*SZ%mod)%mod*sz[j]%mod);
    		}
    	}
    	return 0;
    }
    

XLVII.网吧里的波特

给定两个 ABs,t

你可以花费 c+aA+bB 的代价,翻转 s 中一个长度为 23 的区间,其中:

  • c,a,b 为确定的整常数,且保证 1ab2,0c500
  • A,B 分别为翻转区间中的 A,B 的数量。

使用最小的代价使得 s=t

保证两串中 A,B 的数量分别相等。

数据范围:多测,n1000

考虑 AABB 要变成 BBAA,我们发现应该使用 AAB->BAA 的翻转两次而非 ABB->BBA 的翻转:因为 ab 所以翻转 AAB 总是不劣于 BBA

翻转 AABBAA 的操作等价于将 B 越过两个 A。于是我们自然考虑将两个序列中的 B 间匹配,则可以预想地,令 di 为第 i 组匹配两个 B 的位置之差,则有一个式子是

i=1di2(c+2a+b)+(dimod2)(c+a+b)

但是其实这个式子取不到:当两个 B 的起讫点呈逆序对,则其会迎面撞上:

  • BAB 地撞上(因为 B 一次是动两步)。我们发现,其实这种碰撞理论上是不优的:交换碰撞的两个 B 的终点,结果肯定不更劣,且交换后的方案亦会被我们考虑到,所以可以直接忽略这种碰撞。
  • BB 地撞上。我们发现,我们以为这两个 B 全程都是跳 AAB,BAA(除了最后 mod2 的部分;但是我们认为,既然交换了,那么在交换的位置必然是 AAB,BAA 跳),但是实际上在碰撞处其是 ABB 地跳而非 AAB 地跳,还要计算额外的 (ba)

于是我们修正我们的式子为

(i=1di2(c+2a+b)+(dimod2)(c+a+b))+(ba)Inversions

同时,依照我们上述分析,我们可以发现,所有奇数位的配对位总是递增的,偶数位同理:这是因为,不递增就会出现 BAB 碰撞。

为什么奇偶间可以不递增呢?因为有时在奇偶间交换,虽然可能要多一次 ABB 地跳,但是却能省掉两次 AB 地跳,此时是更优的。

于是可以 DP:设一个状态表示 t 中一段前缀中的 B 匹配了若干奇数 B 和若干偶数 B,逆序对的计算可以在 DP 的过程中一并计算,转移就枚举下一个 B 配奇数还是配偶数即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a,b,c,res;
char s[810],t[810];
vector<int>u,v;
int pos[810],sop[810];
int f[810],g[810];
void mina(){
	scanf("%d%d%d%d",&n,&a,&b,&c);
	scanf("%s%s",s,t);
	for(int i=0;i<n;i++)if(s[i]=='B'){
		if(!(i&1))pos[u.size()]=v.size(),u.push_back(i);
		else sop[v.size()]=u.size(),v.push_back(i);
	}
	int m=0;f[m]=0;
	for(int i=0;i<n;i++)if(t[i]=='B'){
		memset(g,0x3f,sizeof(g));
		for(int j=0;j<=m;j++){
			int k=m-j;
			if(j>u.size()||k>v.size())continue;
			if(j<u.size()){
				int val=f[j];
				int d=abs(u[j]-i);
				// printf("<%d>\n",d);
				val+=(d>>1)*(c+2*a+b)+(d&1)*(c+a+b);
				int p=pos[j];
				val+=max(0,k-p)*(b-a);
				// printf("%d:%d->%d:%d\n",i,j,j+1,val);
				g[j+1]=min(g[j+1],val);
			}
			if(k<v.size()){
				int val=f[j];
				int d=abs(v[k]-i);
				val+=(d>>1)*(c+2*a+b)+(d&1)*(c+a+b);
				int p=sop[k];
				val+=max(0,j-p)*(b-a);
				// printf("%d:%d->%d:%d\n",i,j,j,val);
				g[j]=min(g[j],val);
			}
		}
		m++;
		for(int j=0;j<=m;j++)f[j]=g[j];
	}
	printf("%d\n",f[u.size()]);
	u.clear(),v.clear();
}
int T,id;
int main(){
	freopen("bottle.in","r",stdin);
	freopen("bottle.out","w",stdout);
	scanf("%d%d",&T,&id);
	while(T--)mina();
	return 0;
}
posted @   Troverld  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示