ことばがありあ|

trsins

园龄:3年10个月粉丝:18关注:2

【杂题选讲】2023下半年合集

陽だまりの小部屋。

回忆。

深沉的单词啊,,。

时间不多了。

  • 给出一个 1n1n 的排列 aa,同时有 nn 个点构成的无向图,点的编号从 11nn。如果排列 aa 同时满足 ai>ajai>aji<ji<j,那么图中有一条连接点 ii 和点 jj 的边。
    如果按照以上规则生成的图是二分图,则称该排列为二分图排列。现在你可以选择任意数量(可以为 00)的 ii,将 aiai 改为 aiai
    请问能否通过这种操作将排列变为二分图排列?如果能,输出操作后的二分图排列中,字典序最小的一个。n106n106

二分图没奇环,所以排列中没有长度为 33 的下降子序列。我们对于所有满足 ai>aj,i<jai>aj,i<jjj 拎出来,那么这些 jj 构成的子序列显然是单调上升的(否则就存在 ai1>aj1>aj2ai1>aj1>aj2),对于剩下的也是形成了一个单调上升子序列。这个和题目要求显然是充要的。

那么 dpi,0/1dpi,0/1 表示其中一个子序列(哪个不重要)当前以 ii 结尾且 aiai 符号为负/正,另外一个子序列末项的最大值。然后转移便枚举 dpi1dpi1 在哪个子序列中,以及是否取反,共 44 中情况。

对于输出我们发现题目要求字典序最小,那我们 dp 白做了重新改为从后往前dp,当前到 ii,另一个子序列首项的最大值。转移同理。那么对于方案直接从前往后看能不能放负数,能放就放,注意时刻保证子序列的单调上升。O(n)O(n)

dp=-inf;
dp[n][0]=dp[n][1]=inf;
for(int i=n-1;i;i--){
	for(int j=0;j<2;j++){
		for(int k=0;k<2;k++){
			int x=a[i]*(j?1:-1),y=a[i+1]*(k?1:-1);
			if(x<y)dp[i][j]=max(dp[i][j],dp[i+1][k]);//同个子序列 
			if(x<dp[i+1][k])dp[i][j]=max(dp[i][j],y);//不同子序列 
		}
	}
	if(dp[i][0]==-inf&&dp[i][1]==-inf)NO;
}
YES;
A=B=-inf;//维护两个子序列当前的末项 
for(int i=1;i<=n;i++){
	if(-a[i]>A&&dp[i][0]>B)A=-a[i],out(-a[i]);
	else if(-a[i]>B&&dp[i][1]>A)B-=a[i],out(-a[i]);
	else{
		if(a[i]>A)A=a[i];else B=a[i];
		out(a[i]);
	}
	if(A<B)swap(A,B);
}

2023.7.4 白鸟。

  • nn 个开区间并保证它们都包含某个整点 xx。求最少的移动次数使所有区间不交。一次移动定义为将区间整体左移/右移。n5×103n5×103

必定存在一个区间其不动,情况必定不劣。我们定义该区间为不动点。剩余的区间相当于要么移到不动点左边要么右边。

考虑总代价:

Cost=(rpilo+i×lenpi)+(rolqi+i×lenqi)=(rpi+i×lenpi)+(lqi+i×lenqi)+(lo×|P|+ro×|Q|)Cost=(rpilo+i×lenpi)+(rolqi+i×lenqi)=(rpi+i×lenpi)+(lqi+i×lenqi)+(lo×|P|+ro×|Q|)

其中 oo 表示不动点,而 p,qp,q 表示左边的区间从左到右、右边的区间从右到左。那么显然是区间长度越小的越靠近 oo

dpi,jdpi,j 表示前 ii 个区间中有 jj 个区间放在左边的最小代价。可以枚举不动点然后 O(n3)O(n3) dp。

考虑 |P||P||Q||Q| 的关系。考虑操作完后 oo 移动,其每向左移动一次产生的代价为 |Q|+1|P||Q|+1|P|。那么若 |P||Q||P||Q|,则 oo 必定会移动,与假设存在一个区间不动矛盾。故左右两边区间个数应是相同的。此时 oo 的贡献便是 n12×lenon12×leno

所以在 dp 上增加一维 0/10/1 表示是否加入了不动点(即是否计算其贡献)。

对于 nn 是偶数的情况加入区间 (0,0)(0,0),在计算 oo 的贡献时记得减去 lolo,也就是 (0,0)(0,0) 会走到 lolo 的贡献。

O(n2)O(n2)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=5e3+4;
const int inf=1e17;

int n,ans=inf,dp[N][N][2],fl;

struct node{
	int l,r;
}p[N];

inline bool cmp(node a,node b){
	return a.r-a.l>b.r-b.l;
}

inline void Min(int&x,int y){
	x>y?x=y:x;
	return;
}

signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld%lld",&p[i].l,&p[i].r);
	if(n%2==0)p[++n]={0,0},fl=1;
	for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)dp[i][j][0]=dp[i][j][1]=inf;
	sort(p+1,p+n+1,cmp);
	dp[0][0][0]=0;
	for(int i=1;i<=n;i++){
		int len=p[i].r-p[i].l,lim=min(i,(n-1)/2);
		for(int j=0;j<=lim;j++){
			if(j)Min(dp[i][j][0],dp[i-1][j-1][0]+(j-1)*len+p[i].r);
			if(i-j-1>=0)Min(dp[i][j][0],dp[i-1][j][0]+(i-j-1)*len-p[i].l);
			if(j)Min(dp[i][j][1],dp[i-1][j-1][1]+(j-1)*len+p[i].r);
			if(i-j-1>=0)Min(dp[i][j][1],dp[i-1][j][1]+(i-2-j)*len-p[i].l);
			Min(dp[i][j][1],dp[i-1][j][0]+len*(n-1)/2+(fl?p[i].l:0));
		}
	}
	printf("%lld\n",dp[n][(n-1)/2][1]);
	return 0;
}
  • CF1749F Distance to the Path

  • 给你一棵树,点有点权,初始为 00,两种操作:
    1.询问点权
    2.将所有到uuvv 的路径的距离不超过 kk 的点权值加上 valval
    n,Q2×105,k20n,Q2×105,k20

更好的理解请看:https://www.luogu.com.cn/blog/310801/cf1749f-distance-to-the-path-ti-xie

考虑如果不是到一条路径的距离而是到一个点怎么做。

发现 kk 很小。于是一个自然且朴素的想法就是做一个 fu,kfu,k 维护 uu 子树内距离点 ukuk 的所有点要加上多少,对于查询便暴力往上跳至多 2020 次,加上他们对应到点 uuff。发现这样对于 uu 的祖先的情况不好处理,考虑容斥的思想,改为距离正好是 kk 的所有点。对于距离 ukuk 的且在子树外的点考虑用祖先去覆盖。

那么先处理 fu,kfu,k,对于 ffauffau 会发现如果是 ffau,kffau,k,能够覆盖到 uuk1k1 层儿子,这是好的;但是子树外会存在点 vv 使 disu,v=k+1>kdisu,v=k+1>k,但 vv 却被打上标记。而对于 ffau,k1ffau,k1 能够避免外部点到 uu 距离大于 kk 的情况,却难以覆盖 uuk1k1 层儿子。

自然会想到容斥。但是容斥好烦。不要容斥。考虑“算2次,跳1次”的方法:处理 fu,k,fu,k1fu,k,fu,k1,然后是 ffau,k1,ffau,k2ffau,k1,ffau,k2,容易发现这样能够完美覆盖全部距离 ukuk 的节点。往上最多跳 kk 次,跳到 rtrt 时记得将剩下的 ff 全部处理。

回归本题在路径上的情况:某个点到达路径的最终点必然要么是从下往上,要么从上往下。前者可以在 uvuv 路径上任何一个节点 pp 的子树中出现,后者必然是 lca(u,v)lca(u,v) 的祖先的情况。

考虑一样的做法,先树链剖分,对于每个 kk 建一棵线段树,修改操作时先修改 uvuv 上所有点的 fnode,kfnode,k,然后找到 lca 后往上执行“算2次,跳1次”的操作,复杂度为 O(qlog2n+dqlogn)O(qlog2n+dqlogn)

#include<bits/stdc++.h>
using namespace std;

#define int long long
#define ls p<<1
#define rs p<<1|1

const int N=2e5+6;

int n,Q,tot,head[N],dep[N],sz[N],son[N],top[N],idx,dfn[N],rev[N],tr[N<<2][22],tag[N<<2][22],fa[N];

struct edge{
	int to,nxt;
}e[N<<1];

inline void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

inline void dfs1(int u,int faa){
	fa[u]=faa,sz[u]=1,dep[u]=dep[faa]+1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==faa)continue;
		dfs1(v,u);
		sz[u]+=sz[v];
		if(sz[v]>sz[son[u]])son[u]=v;
	}
	return;
}

inline void dfs2(int u,int tp){
	top[u]=tp,dfn[u]=++idx,rev[idx]=u;
	if(son[u])dfs2(son[u],tp);
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
	}
	return;
}

inline int lca(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])swap(u,v);
		u=fa[top[u]];
	}
	return dep[u]<dep[v]?u:v;
}

inline void pushup(int p,int k){
	tr[p][k]=tr[ls][k]+tr[rs][k];
	return;
}

inline void pushdown(int p,int l,int r,int k){
	if(!tag[p][k])return;
	int mid=(l+r)>>1;
	tr[ls][k]+=tag[p][k]*(mid-l+1),tr[rs][k]+=tag[p][k]*(r-mid);
	tag[ls][k]+=tag[p][k],tag[rs][k]+=tag[p][k],tag[p][k]=0;
	return;
}

inline void update(int p,int l,int r,int s,int t,int k,int val){
	if(s<=l&&r<=t){
		tr[p][k]+=val*(r-l+1),tag[p][k]+=val;
		return;
	}
	pushdown(p,l,r,k);
	int mid=(l+r)>>1;
	if(s<=mid)update(ls,l,mid,s,t,k,val);
	if(t>mid)update(rs,mid+1,r,s,t,k,val);
	pushup(p,k);
	return;
}

inline int query(int p,int l,int r,int ps,int k){
	if(l==r)return tr[p][k];
	int mid=(l+r)>>1;
	pushdown(p,l,r,k);
	if(ps<=mid)return query(ls,l,mid,ps,k);
	else return query(rs,mid+1,r,ps,k);
}

inline void upd(int u,int v,int k,int val){
	if(k<0)return;
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])swap(u,v);
		update(1,1,n,dfn[top[u]],dfn[u],k,val);
		u=fa[top[u]];
	}
	if(dep[u]<dep[v])swap(u,v);
	update(1,1,n,dfn[v],dfn[u],k,val);
	return;
}

signed main(){
	scanf("%lld",&n);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%lld%lld",&u,&v);
		addedge(u,v),addedge(v,u);
	}
	dfs1(1,0),dfs2(1,1);
	scanf("%lld",&Q);
	while(Q--){
		int op,u,v,k,val,ans=0,o;
		scanf("%lld",&op);
		if(op==1){
			scanf("%lld",&u);
			for(int i=0;i<=20;i++){
				ans+=query(1,1,n,dfn[u],i);
				u=fa[u];
				if(!u)break;
			}
			printf("%lld\n",ans);
		}
		else{
			scanf("%lld%lld%lld%lld",&u,&v,&val,&k),o=lca(u,v);
			upd(u,v,k,val),upd(o,o,k-1,val);
			for(int i=k-1;i>=0;i--){
				if(o==1){
					for(int j=i-1;j>=0;j--)upd(o,o,j,val);
					break;
				}
				else o=fa[o],upd(o,o,i,val),upd(o,o,i-1,val);
			}
		}
	}
	return 0;
}
  • 2020 ICPC Asia Shenyang Regional,J - Descent of Dragons

  • 有一个长度为 nn 的初始全部为 00 的序列 aaQQ 次操作,每次操作分为:1. l,r,xl,r,x 表示 lir,ai+=[ai=x]lir,ai+=[ai=x];2. l,rl,r 表示询问区间 aiai 的max。n,Q5×105n,Q5×105

对于每个 ai=xai=x 建立一棵线段树,那么修改操作相当于将第 xx 棵树上的 [l,r][l,r] 区间删除后粘贴到第 x+1x+1 棵树上。查询时 [l,r][l,r] 会在很多棵树上出现,时间复杂度就寄了。不会做啊!怎么办!

改为对于 aixaix 建立线段树。那么每次修改只需要将 [l,r][l,r] 复制到第 x+1x+1 棵树。那么 [l,r][l,r] 会转为出现在一段前缀上。我会二分!O(nlog2n)O(nlog2n)

#include <bits/stdc++.h>
using namespace std;

const int N=5e5+6;
const int M=1e7+8;

int n,m,Q,tr[M],ls[M],rs[M],rt[N],idx;

inline void pushup(int p){
	tr[p]=tr[ls[p]]+tr[rs[p]];
	return;
}

inline void build(int&p,int l,int r){
	if(!p)p=++idx;
	if(l==r){
		tr[p]=1;
		return;
	}
	int mid=(l+r)>>1;
	build(ls[p],l,mid),build(rs[p],mid+1,r);
	pushup(p);
	return;
}

inline void copy(int&p,int q,int l,int r,int s,int t){
	if(!q)return;
	if(s<=l&&r<=t){
		p=q;
		return;
	}
	else idx++;
	int mid=(l+r)>>1;
	ls[idx]=ls[p],rs[idx]=rs[p],tr[idx]=tr[p],p=idx;
	if(s<=mid)copy(ls[p],ls[q],l,mid,s,t);
	if(t>mid)copy(rs[p],rs[q],mid+1,r,s,t);
	pushup(p);
	return;
}

inline int query(int p,int l,int r,int s,int t){
	if(s<=l&&r<=t)return tr[p];
	int mid=(l+r)>>1,res=0;
	if(s<=mid)res+=query(ls[p],l,mid,s,t);
	if(t>mid)res+=query(rs[p],mid+1,r,s,t);
	return res;
}

int main(){
	scanf("%d%d",&n,&Q);
	build(rt[0],1,n);
	for(int i=1;i<=Q;i++){
		int op,l,r,x=0;
		scanf("%d%d%d",&op,&l,&r);
		if(op==1){
			scanf("%d",&x);
			copy(rt[x+1],rt[x],1,n,l,r);
		}
		else{
			int L=0,R=i;
			while(L<=R){
				int mid=(L+R)>>1;
				if(query(rt[mid],1,n,l,r))L=mid+1,x=mid;
				else R=mid-1;
			}
			printf("%d\n",x);
		}
	}
	return 0;
}
  • [ABC221H] Count Multiset

  • 求多少个大小为 kk,集合数之和为 nn,且一个数出现次数 mm 的所有集合。n,m5×103n,m5×103

没有脑子选手会转化为一个长度为 kk 的不降序列,和为 kk,不出现连续 mm 个相同的数。

一种常规的做法是差分来去除“不降”这个限制,令 bi=aiai1bi=aiai1,有 bi(ki+1)=nbi(ki+1)=n,由于 bb 之间没有位置顺序限制所以 reverse 一下,变为 ibi=nibi=n。设 fi,jfi,j 表示当前在 bibi,和为 jj

fi,j=i1p=im+1j/iB=1fp,jiBfi,j=i1p=im+1j/iB=1fp,jiB

其中 pp 为枚举上一个不同的数的位置,BB 即枚举 bibiBB 是调和级数,可以前缀和优化至 O(n2lnn)O(n2lnn)

没脑子选手不会 dp 怎么办?发现题目去掉 mm 的限制就是分拆数。而对于 mm 的限制相当于数列前 m+1m+1 项都是 11,方案数也就是 pij,j(m+1)pij,j(m+1)。所以和分拆数一样 dp 就完事了。O(n2)O(n2)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=5e3+4;
const int mod=998244353;

int n,m,dp[N][N],sdp[N][N];

signed main(){
	scanf("%d%d",&n,&m);
	dp[0][0]=sdp[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int k=i;k<=j;k+=i){
				dp[i][j]=(dp[i][j]+sdp[i-1][j-k])%mod;
				if(i>m)dp[i][j]=(dp[i][j]-sdp[i-m-1][j-k]+mod)%mod;
			}
			sdp[i][j]=(sdp[i-1][j]+dp[i][j])%mod;
		}
		sdp[i][0]=1;
	}
	for(int i=1;i<=n;i++)printf("%d\n",dp[i][n]);
	return 0;
}
  • CF1842G Tenzing and Random Operations

  • 有一个长度为 nn 的序列 aa,定义其权值为所有元素的乘积。有 mm 次操作,每次等概率选择一个 ii 然后将序列 aa[i,n][i,n] 这个后缀全部加 vv。求最后序列 aa 权值的期望。n5000,m,v109n5000,m,v109

boring。将 mm 次操作与长度为 nn 的序列合起来变为 (m+1)×n(m+1)×n 的矩阵 MM,其中 Mi,jMi,j 表示第 ii 次操作选择了第 jj 个位置加上 vv。每一行都是形如一段前缀 00 以后接着一段后缀 vv。总期望就是 Mi,jMi,j,也就是每一列的和的乘积。那这玩意相当于一个 (t1+t2+...)(t1+t2+...)(t1+t2+...)...(t1+t2+...)(t1+t2+...)(t′′1+t′′2+...)... 的一个东西,直接拆贡献就是第 ii 个位置在每次操作时选择 0/v/ai0/v/ai。选择 00 贡献就没了就不管,也就是每次要么选 vv 要么就是 aiai

fi,jfi,j 表示 [1,i][1,i] 选择好了且被这些元素选择的行共 jj 行。转移枚举 i+1i+1 的决策:

  1. 选择 ai+1ai+1fi,j×ai+1fi+1,jfi,j×ai+1fi+1,j
  2. 选择之前选过的 jj 行中的 vvfi,j×jvfi+1,jfi,j×jvfi+1,j
  3. 选择不在 jj 行中的 vvfi,j×(mj)vfi+1,j+1fi,j×(mj)vfi+1,j+1

总期望即为 fn,jnmjnmfn,jnmjnmO(n2)O(n2)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int mod=1e9+7;
const int N=5e3+4;

int n,m,v,ans,dp[N][N],a[N];

inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

signed main(){
	scanf("%lld%lld%lld",&n,&m,&v),dp[0][0]=1;
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		for(int j=0;j<=min(n,m);j++){
			dp[i][j]=(dp[i][j]+dp[i-1][j]*(a[i]+j*v%mod)%mod)%mod;
			dp[i][j+1]=(dp[i][j+1]+dp[i-1][j]*(m-j)%mod*i%mod*v%mod)%mod;
		}
	}
	for(int i=0;i<=min(n,m);i++)ans=(ans+dp[n][i]*qpow(n,m-i)%mod)%mod;
	printf("%lld",ans*qpow(qpow(n,m),mod-2)%mod);
	return 0;
}
  • [AGC058D] Yet Another ABC String

  • 求有多少长度为 a+b+ca+b+c 的只包含 ABC 三个字母的字符,满足恰好有 aaAbbBccC,且不包含形如 ABCBCACAB 的连续子串。a,b,c106a,b,c106

挺nb的。若 [i,i+2],[i+2,i+4][i,i+2],[i+2,i+4] 不合法,那么 [i+1,i+3] 也不合法。看起来很蠢对吧,但是就是这么蠢,容斥有 s 个段 [i,i+2] 不合法,那么 [i+1,i+3] 必须合法。做完了!O(n)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int mod=998244353;
const int N=3e6+7;

int n,k,A,B,C,fac[N],ifac[N],pw[N],ans;

inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

inline void init(int n){
	fac[0]=ifac[0]=pw[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod,pw[i]=pw[i-1]*2%mod;
	ifac[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
	return;
} 

inline int c(int n,int m){
	if(n<m||n<0||m<0)return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

signed main(){
	scanf("%lld%lld%lld",&A,&B,&C),n=A+B+C,k=min(A,min(B,C));
	init(n),ans=c(n,A)*c(n-A,B)%mod;
	for(int i=1;i<=k;i++){
		int m=n-3*i,t=c(m,A-i)*c(m-(A-i),B-i)%mod;
		if(i&1)ans=(ans-(t*c(m+i,i)%mod*pw[i]%mod+t*c(m+i-1,i-1)%mod*pw[i-1]%mod)%mod+mod)%mod;
		else ans=(ans+(t*c(m+i,i)%mod*pw[i]%mod+t*c(m+i-1,i-1)%mod*pw[i-1]%mod)%mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}
  • loj6077 「2017 山东一轮集训 Day7」逆序对

  • 求有多少长度为 n 的排列使其逆序对对数为 kn,k105

pbyyds。

相信大家都会 O(n2)。我们发现从小到大插入 i 时产生新的逆序对个数 xi[0,i1] 且每种情况唯一。那么题目就是求 xi=k 的方案数了,其中 xi[0,i1]

相信大家都会容斥。将其转化为

S(1)|S|R(kiSi)

其中 S 为钦定的不满足条件的 i 的集合,那么对于 iSxii,xii0。而不属于 S 的限制为 xi0。定义 iS,yi=xii,那么属于 Syi 和不属于 Sxi 共同限制都是 0

所以 R(j) 含义即为 (ni=1zi)=j 的方案数,其中 zi0。那么这个大家都会插板法,所以问题转化为如何快速求出 iSi

考虑设计dp:fi.j 表示从 1n 中选择 i 和不同的数且和为 j 的方案数。

不妨令此时我们选择了 a1<a2<<ai,那么令 x 轴为下标、y 轴为 aI 的值,那么 j 即为一个形如从左下至右上增长的一个阶梯形的面积。

考虑转移。为了使转移不重不漏,我们钦定只能向下/左转移。我们可以给 a 集体加一,那么相当于在阶梯下方垫高一层,转移为 fi,jfi,j+i。此时显然 a 仍满足两两不同。

考虑对于 i 这维的转移。在垫高一层后考虑在最左侧增加一个高度为 1 的列,此时便形如 1,a1,a2,,ai,仍满足两两不同。转移为 fi,jfi+1,j+i+1

注意到最右侧的一列高度可能会大于 n,那么减去这一列即为不合法的方案,是 fi1,jn1

注意 j 范围 k,而 j 为阶梯面积,故 i 范围是 O(k) 的。总时间复杂度 O(kk)

#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=1e5+6;
const int M=470;
const int mod=1e9+7;

int n,k,dp[M][N],fac[N<<1],ifac[N<<1],ans;

inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

inline void init(int n){
	fac[0]=ifac[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
	ifac[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
	return;
}

inline int C(int n,int m){
	if(n<m||n<0||m<0)return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

signed main(){
	scanf("%lld%lld",&n,&k),init(N*2-1);
	dp[0][0]=1;
	for(int i=1;i<=453;i++)for(int j=1;j<=k;j++){
		if(j>=i)dp[i][j]=(dp[i][j-i]+dp[i-1][j-i])%mod;
		if(j>n)dp[i][j]=(dp[i][j]-dp[i-1][j-n-1]+mod)%mod;
	}
	for(int i=0;i<=453;i++)for(int j=0;j<=k;j++)ans=(ans+(i&1?mod-1:1)*dp[i][j]%mod*C(k-j+n-1,n-1)%mod)%mod;
	printf("%lld",ans);
	return 0;
}
  • gaze

  • 给定序列 a,支持单点修改,查询给定 x 求所有值 x 的位置形成的连通块个数。n2×105

连通块个数=分界点/2。定义分界点为 a,b 之间表示 ax,b<x 或反之。

使用树状数组维护分界点个数,对于 ax 时加一,b>x 时减一。

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+6;

int n,m,lsh[N<<1],a[N],tot,tr[N<<2];
struct node{int op,x,y;}q[N];

inline int lowbit(int x){return x&-x;}
inline void add(int x,int val){while(x<=tot)tr[x]+=val,x+=lowbit(x);}
inline int qry(int x){int res=0;while(x)res+=tr[x],x-=lowbit(x);return res;}

inline void upd(int x,int op){
	int l=a[x-1],r=a[x];
	if(l>r)swap(l,r);l++;
	add(l,op),add(r+1,-op);
	return;
}

int main(){
	scanf("%d%d",&n,&m),lsh[++tot]=0;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),lsh[++tot]=a[i];
	for(int i=1;i<=m;i++){
		scanf("%d",&q[i].op);
		if(q[i].op==1)scanf("%d",&q[i].x),lsh[++tot]=q[i].x;
		else scanf("%d%d",&q[i].x,&q[i].y),lsh[++tot]=q[i].y;
	}
	sort(lsh+1,lsh+tot+1),tot=unique(lsh+1,lsh+tot+1)-lsh-1;
	for(int i=0;i<=n+1;i++)a[i]=lower_bound(lsh+1,lsh+tot+1,a[i])-lsh;
	for(int i=1;i<=n+1;i++)upd(i,1);
	for(int i=1;i<=m;i++){
		if(q[i].op==1){
			q[i].x=lower_bound(lsh+1,lsh+tot+1,q[i].x)-lsh;
			printf("%d\n",qry(q[i].x)/2);
		}
		else{
			q[i].y=lower_bound(lsh+1,lsh+tot+1,q[i].y)-lsh;
			upd(q[i].x,-1),upd(q[i].x+1,-1),a[q[i].x]=q[i].y;
			upd(q[i].x,1),upd(q[i].x+1,1);
		}
	}
	return 0;
}
  • CF799F Beautiful fountains rows

  • 长度为 m 的数轴上有每个区间,每个区间都有各有一种颜色且颜色不同(即该区间内每个点染上区间编号对应的颜色,一个点可以存在多个颜色,区间颜色不给定)。定义一个区间 [l,r] 是好的当且仅当其至少包含一种颜色,且对于其包含在内的每种颜色,颜色出现次数为奇数。n,m106

出现奇数次和可以出现 0 次看似有点冲突,将将题目改成出现次数为偶数次。

一个经典的trick:使用异或和来判断奇偶,给每一个区间随机一个权值即颜色,则当选择的区间中所有点的异或和为零时,每种颜色都出现偶数次,再使用前缀异或和即可维护。

对于把奇数个转为偶数个,将每个区间(包括自己选择的好区间)左侧或右侧 +/-1 即可。

#include<bits/stdc++.h>
using namespace std;

#define int long long

mt19937_64 rnd(time(0));

const int N=1e5+6;

int n,m,a[N],t[N],ans,c;
map<int,int>cnt,mps;

signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++){
		int x=rnd(),l,r;
		scanf("%lld%lld",&l,&r),l++;
		a[l]^=x,a[r+1]^=x;
		t[l-1]++,t[r+1]--;
	}
	for(int i=1;i<=m;i++){
		a[i+2]^=a[i],t[i]+=t[i-1],c++;
		if(t[i])c=0;
		cnt[a[i]]++,mps[a[i]]+=i-1;
		ans+=cnt[a[i]]*i-mps[a[i]]-c*(c+1)/2;
	}
	printf("%lld\n",ans);
}
  • [AGC003D] Anticube

  • 给定 n 个数 ai,要求从中选出最多的数,满足任意两个数之积都不是完全立方数。n105ai1010

先考虑朴素地分解质因数,所有次数对 3 取模,得到 ai=pkii,有 bi=p3kii,则 aibi 为完全立方数。所以将每个数先转为次数 <3 的模式后对于每一对 aibi 看哪个出现的多即可。

但是不能直接分解,但是可以分解 3V 内的质因数。考虑剩下的那个数 di,此时仅有三种可能:p,pq,p2,分类一下即可。

#include<bits/stdc++.h>
using namespace std;

#define ll long long

const int N=1e5+6;
const int M=330;

int n,p[N],pid,cnt[N][M],ans;
bool vis[N],cubic[N];
ll a[N],b[N],d[N],mx;
map<ll,int>mps;
queue<ll>q;

void init(int n){
	for(int i=2;i<=n;i++){
		if(!vis[i])p[++pid]=i;
		for(int j=1;j<=pid&&i*p[j]<=n;j++){
			vis[i*p[j]]=1;
			if(i%p[j]==0)break;
		}
	}
	return;
}

inline bool isq(ll x){
	ll y=sqrt(x);
	return y*y==x;
}

signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),mx=max(mx,a[i]);
	int A=pow(mx,1./3),B=sqrt(mx);
	init(B);
	for(int i=1;i<=n;i++){
		int tmp=1;
		for(int j=1;j<=pid&&p[j]<=A;j++){
			while(a[i]%p[j]==0)a[i]/=p[j],cnt[i][j]++;
			cnt[i][j]%=3;
			if(cnt[i][j])tmp*=p[j];
			if(cnt[i][j]==2)tmp*=p[j];
		}d[i]=a[i],a[i]*=tmp;
		if(a[i]==1)ans=1,cubic[i]=1;
	}
	for(int i=1;i<=n;i++)if(!cubic[i]){
		if(d[i]==1||(d[i]<=B&&!vis[d[i]])||isq(d[i]))
			q.push(i),mps[a[i]]++;
		else ans++;
	}
	while(!q.empty()){
		int i=q.front();q.pop();
		if(isq(d[i]))b[i]=sqrt(d[i]);
		else b[i]=d[i]*d[i];
		for(int j=1;j<=pid&&p[j]<=A;j++){
			if(cnt[i][j])b[i]*=p[j];
			if(cnt[i][j]==1)b[i]*=p[j];
		}
		ans+=max(mps[a[i]],mps[b[i]]),mps[a[i]]=mps[b[i]]=0;
	}
	printf("%d\n",ans);
	return 0;
}
  • P8352 [SDOI/SXOI2022] 小 N 的独立集

  • 给定 n 个点的树的形态以及点的权值范围 k,对于任意 i[1,kn]
    求有多少种权值分配方案,使得树的最大权独立集大小为 in1000,k5

最朴素的暴力即 fu,0/1 表示选或不选,暴力枚举权值可能,O(nkn)。注意到 f 值域为 O(nk) 的,所以可以 dp 套 dp,dpu,x,y 表示 fu,0=x,fu,1=y 时的情况。O(n4k4)

注意到瓶颈在于 dp 状态数就寄了,改变 f 的定义为是否钦定 u 不选,那么有 fu,0fu,1auk,所以有 dpu,x,d,其中 fu,0 被表示为 x+dO(n2k4)

#include <bits/stdc++.h>
using namespace std;

#define int long long

const int N=1e3+4;
const int M=5*N;
const int mod=1e9+7;

int n,k,sz[N],f[N][M][6],g[M][6],tot,head[N];

struct edge{
	int to,nxt;
}e[N<<1];

inline void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

void dfs(int u,int fa){
	sz[u]=1;
	for(int i=1;i<=k;i++)f[u][0][i]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		memset(g,0,sizeof(g));
		for(int i=0;i<=k*sz[u];i++)
		for(int j=0;j<=k;j++)if(f[u][i][j])
		for(int p=0;p<=k*sz[v];p++)
		for(int q=0;q<=k;q++)if(f[v][p][q])
		(g[i+p+q][max(i+j+p,i+p+q)-(i+p+q)]+=f[u][i][j]*f[v][p][q]%mod)%=mod;
		memcpy(f[u],g,sizeof(g)),sz[u]+=sz[v];
	}
	return;
}

signed main(){
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%lld%lld",&u,&v);
		addedge(u,v),addedge(v,u);
	}
	dfs(1,0);
	for(int i=1;i<=n*k;i++){
		int ans=0;
		for(int d=0;d<=min(i,k);d++)ans=(ans+f[1][i-d][d])%mod;
		printf("%lld\n",ans);
	}
	return 0;
}
  • CF1842H Tenzing and Random Real Numbers

  • n[0,1] 范围内的均匀随机变量 x1nm 条限制,每条限制形如 xi+xj1xi+xj1。请你求出所有限制均满足的概率。1n20, 0mn2+n

先将 x[i]-=0.5,变为 xi+xj0,xi[0.5,0.5]。将 xi 按照绝对值大小排序后发现 xi+xj0 等价于 xj0(钦定 |xi||xj|),大于等于同理,所以按照绝对值排序后给每个数安排正负号,那么转为状压 dp,做完了。

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=21;
const int mod=998244353;
const int inv2=(mod+1)/2;

int n,m,g[2][N],dp[1<<N],ans;

inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++){
		int op,x,y;
		scanf("%lld%lld%lld",&op,&x,&y),x--,y--;
		g[op][x]|=(1<<y),g[op][y]|=(1<<x);
	}dp[0]=1;
	for(int s=1;s<(1<<n);s++)for(int i=0;i<n;i++)if(s&(1<<i)){
		if(!(g[0][i]&s))dp[s]=(dp[s]+dp[s^(1<<i)])%mod;
		if(!(g[1][i]&s))dp[s]=(dp[s]+dp[s^(1<<i)])%mod;
	}
	int ans=dp[(1<<n)-1];
	for(int i=1;i<=n;i++)ans=ans*qpow(i,mod-2)%mod;
	printf("%lld",ans*qpow(inv2,n)%mod);
	return 0;
}
  • Toxic Violet Cubes

  • 给图的每⼀个点染上⼀种颜⾊,使得对于所有节点,⾄少有⼀半的邻点的颜⾊都与它不同。求最少⽤多少种颜⾊,并给出任意⼀种⽅案。n5000,m3×104

大胆猜想答案 2。对于 m=0 时答案为 1 否则为 2。怎么证明?

C=(u,v)E|colucolv|,每次从图中随便拿一个不满足条件的点,即超过⼀半的邻点的颜⾊与它不同,将其反色,发现 C 至少 +1。那么 S 单增且有显然上界 m,故必定会终止即得证,且最多 m 轮,每轮容易通过记录每个点周围颜色与其是否相同的值之和做到 O(n),总时间复杂度 O(nm)

#include<bits/stdc++.h>
using namespace std;

const int N=5e3+4;

int n,m,a[N],col[N];
queue<int>q;
vector<int>g[N];

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		g[u].push_back(v),g[v].push_back(u);
		a[u]--,a[v]--;
	}
	if(!m){
		puts("1");
		for(int i=1;i<=n;i++)printf("1 ");
		return 0;
	}
	else puts("2");
	for(int i=1;i<=n;i++)q.push(i);
	while(!q.empty()){
		int u=q.front();q.pop();
		if(a[u]>=0)continue;
		col[u]^=1,a[u]*=-1;
		for(int v:g[u]){
			a[v]+=(col[u]!=col[v]?1:-1)*2;
			if(a[v]<0)q.push(v);
		}
	}
	for(int i=1;i<=n;i++)printf("%d ",col[i]+1);
	return 0;
}
  • CF1553H XOR and Distance

  • 一个长度为 n 的序列 {ai},保证 i[1,n],ai[0,2k),要你对所有 x[0,2k),求出 mini,j[1,n]ij|(aix)(ajx)|k19,n2k

  • P9595 「Daily OI Round 1」Xor

  • 给定一个长度为 n 的序列,一共有 q 次询问,每次询问给定正整数 x,然后将全局异或 x 后求最长连续值域区间。n,q5×105

这两题对于我的做法而言没有区别,所以放在一起讲。

先把 trie 树建出来(实际上不需要真正建出),异或上 x 相当于将 x 拆成二的幂次的和后每次异或一个二次幂。在 trie 上异或 2k 是好做的,相当于将从下往上(从 0 标号)的第 k 层两两相邻区间交换。

在每个节点上维护题目需要的信息,以 CF1553H 为例,我们维护该节点表示的区间的答案、最大值与最小值,那么节点的答案可以被左子树的最大值与右子树最小值的差值来转移。

注意对于某一层有影响的只有它下面的层,所以考虑这样的一个dp:

fi,j,k 表示第 i 层的第 j 个区间,第 [0,i1] 层的状态(0 表示不交换,1 交换)。转移是简单的,代码中的 j,k 两维调换了一下。

对于P9595 则更简单些,维护以左端点为起点和以右端点为终点的极长连续段即可。

//CF1553H
#include<bits/stdc++.h>
using namespace std;

const int inf=1e9;

int n,K;
vector<vector<int>>f[21],mn[21],mx[21];

int main(){
	scanf("%d%d",&n,&K);
	for(int i=0;i<=K;i++){
		mn[i].resize(1<<i),mx[i].resize(1<<i),f[i].resize(1<<i);
		for(int j=0;j<(1<<i);j++)mn[i][j].resize(1<<K-i),mx[i][j].resize(1<<K-i),f[i][j].resize(1<<K-i);
	}
	for(int i=0;i<(1<<K);i++)f[0][0][i]=mn[0][0][i]=inf,mx[0][0][i]=-inf;
	for(int i=1;i<=n;i++){
		int x;scanf("%d",&x);
		mn[0][0][x]=mx[0][0][x]=x;
	}
	for(int i=1;i<=K;i++){
		for(int j=0;j<(1<<i-1);j++){
			for(int k=0;k<(1<<K-i);k++){
				int J=j|(1<<i-1);
				f[i][j][k]=min(min(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),mn[i-1][j][k<<1|1]-mx[i-1][j][k<<1]);
				f[i][J][k]=min(min(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),mn[i-1][j][k<<1]-mx[i-1][j][k<<1|1]+(1<<i));
				mn[i][j][k]=min(mn[i-1][j][k<<1],mn[i-1][j][k<<1|1]);
				mn[i][J][k]=min(mn[i-1][j][k<<1|1]-(1<<i-1),mn[i-1][j][k<<1]+(1<<i-1));
				mx[i][j][k]=max(mx[i-1][j][k<<1|1],mx[i-1][j][k<<1]);
				mx[i][J][k]=max(mx[i-1][j][k<<1]+(1<<i-1),mx[i-1][j][k<<1|1]-(1<<i-1));
			}
		}
	}
	for(int i=0;i<(1<<K);i++)printf("%d ",f[K][i][0]);
	return 0;
}
//P9595
#include<bits/stdc++.h>
using namespace std;

const int _=19;

int n,Q,now;
vector<vector<int>>f[21],l[21],r[21];

int main(){
	scanf("%d%d",&n,&Q);
	for(int i=0;i<=_;i++){
		l[i].resize(1<<i),r[i].resize(1<<i),f[i].resize(1<<i);
		for(int j=0;j<(1<<i);j++)l[i][j].resize(1<<_-i),r[i][j].resize(1<<_-i),f[i][j].resize(1<<_-i);
	}
	for(int i=1;i<=n;i++){
		int x;scanf("%d",&x);
		l[0][0][x]=r[0][0][x]=f[0][0][x]=1;
	}
	for(int i=1;i<=_;i++){
		for(int j=0;j<(1<<i-1);j++){
			for(int k=0;k<(1<<_-i);k++){
				int J=j|(1<<i-1);
				f[i][j][k]=max(max(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),l[i-1][j][k<<1|1]+r[i-1][j][k<<1]);
				f[i][J][k]=max(max(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),l[i-1][j][k<<1]+r[i-1][j][k<<1|1]);
				l[i][j][k]=max(l[i-1][j][k<<1],l[i-1][j][k<<1]==(1<<i-1)?l[i-1][j][k<<1]+l[i-1][j][k<<1|1]:0);
				l[i][J][k]=max(l[i-1][j][k<<1|1],l[i-1][j][k<<1|1]==(1<<i-1)?l[i-1][j][k<<1|1]+l[i-1][j][k<<1]:0);
				r[i][j][k]=max(r[i-1][j][k<<1|1],r[i-1][j][k<<1|1]==(1<<i-1)?r[i-1][j][k<<1|1]+r[i-1][j][k<<1]:0);
				r[i][J][k]=max(r[i-1][j][k<<1],r[i-1][j][k<<1]==(1<<i-1)?r[i-1][j][k<<1]+r[i-1][j][k<<1|1]:0);
			}
		}
	}
	while(Q--){
		int x;scanf("%d",&x),now^=x;
		printf("%d\n",f[_][now][0]);
	}
}
  • CF1149D Abandoning Roads

  • 一张 n 个点 m 条边的无向图,只有 a,b 两种边权(a<b),对于每个 i,求图中所有的最小生成树中,从 1i 距离的最小值。n70,m200

考虑建出最小生成树的过程,显然是先选择完 a 边后才会考虑 b 边,那么在选择 b 边之前显然这个图被分为了若干个连通块,块内两两之间都是 a 边,后文的“连通块”均代指此处的 a 边连通块,通过 dfs 给连通块染色是容易的。

对于一条可能在最小生成树上出现的路径,显然它不会离开一个连通块后又回来(因为是树没有环),所以一个朴素的 dp 就是 dpS,u 表示已经遍历过了 S 这个集合内的连通块,当前在点 u,转移便是最短路问题,放在 dijkstra 上跑可以获得 O(2n(n+m)log2n(n+m)) 的时间复杂度。

瓶颈在于 2n 上,而 n70 启发我们状压,如何压缩状态?注意到对于大小 3 的连通块由于内部最长路径也只有 2a,而 2a<2b,故在最短路的流程中不会回到此连通块。所以这种连通块的状态是没有用的,故只记录大小 >3 的连通块即可,时间复杂度为 O(2n4(n+m)log2n4(n+m))

#include<bits/stdc++.h>
using namespace std;

#define ppi pair<int,pair<int,int>>
#define mp make_pair
#define fi first
#define se second

const int N=72;
const int M=220;
const int P=(1<<18)+1;

int n,m,tot,cnt,a,b,head[N],col[N],idx[2],vis[N],typ[N],dp[P][N];

struct edge{
	int to,nxt,w;
}e[M<<1];

inline void addedge(int u,int v,int w){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	e[tot].w=w;
	return;
}

void dfs(int u){
	cnt++,vis[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(vis[v]||e[i].w==b)continue;
		dfs(v);
	}
	return;
}

void Dfs(int u,bool o){
	col[u]=idx[o],typ[u]=o;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(col[v]||e[i].w==b)continue;
		Dfs(v,o);
	}
	return;
}

void Dijkstra(){
	priority_queue<ppi,vector<ppi>,greater<ppi>>q;
	memset(dp,0x3f,sizeof(dp)),dp[0][1]=0;
	q.push(mp(0,mp(0,1)));
	while(!q.empty()){
		int d=q.top().fi,u=q.top().se.se,S=q.top().se.fi;q.pop();
		if(dp[S][u]<d)continue;
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if(col[u]==col[v]&&e[i].w==b)continue;
			if(typ[v]&&(S&(1<<col[v]-1)))continue;
			int T=S;
			if(typ[u]&&col[u]!=col[v])T|=(1<<col[u]-1);
			if(dp[T][v]>d+e[i].w)dp[T][v]=d+e[i].w,q.push(mp(dp[T][v],mp(T,v)));
		}
	}
	return;
}

int main(){
	scanf("%d%d%d%d",&n,&m,&a,&b),idx[0]=n;
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		addedge(u,v,w),addedge(v,u,w);
	}
	for(int i=1;i<=n;i++)if(!vis[i]){
		cnt=0,dfs(i);
		if(cnt>3)idx[1]++,Dfs(i,1);
		else idx[0]++,Dfs(i,0);
	}
	Dijkstra();
	for(int i=1;i<=n;i++){
		int ans=dp[0][i];
		for(int s=1;s<(1<<18);s++)ans=min(ans,dp[s][i]);
		printf("%d ",ans);
	}
	return 0;
}
  • P7450 [THUSCH2017] 巧克力

  • 给定一个 n×m 的表格,每个格子有一个权值 ai,j 和颜色 ci,jci,j=1 表示这个格子不可用。问最小的四连通块大小,使得该连通块内没有不可用的格子,且包含至少 k 种不同的颜色。在大小最小的前提下,求出连通块内权值中位数的最小值。n×m233,k5

我们先考虑假如所有点的颜色 [0,k) 的情况。那么这就是斯坦纳树板子。至于中位数,一个经典的想法,便是二分中位数,然后 mid 的点权值设为 inf1,大于的设为 inf+1,那么就容易保证大小最小这一限制的优先级在前面,判断即将权值和对 inf 取模即可,此题中 inf 由于 n,m 范围极小故开 1000 就好了。

现在颜色的取值范围 Ck 的情况,一个极其暴力的想法是枚举 (Ck) 种方案后跑。组合数太大了显然弃掉这个想法。注意到我们上一段中提及的做法时间复杂度非常充裕,O(nm3klognm)5s 的时限里可以重复跑好多次,这启示我们随机化

将每种颜色随机映射到 [0,k) 中。考虑最优解的情况下,选择的颜色必然是恰好 k 种不同颜色。那么在新的图(颜色映射后的图)中我们得到的大小要最小,那么考虑我们最终选择的 k 中颜色,它们必然映射到恰好 k 个不同的新颜色上。否则我们必定不优。所以随机一次的成功概率是 P=k!kk 的。我们如果随机 T 次,失败概率就是 P=(1P)T,当 T=100P2%,保险起见并且由于时限宽裕 T 开到 200否则就会和我一样获得在loj上99分的好成绩。

事实上这个随机颜色的方式即 Color-Coding 还是较为常见的。不出意外的话我会去专门为这个 trick 开个文章。

#include<bits/stdc++.h>
using namespace std;

const int N=234;
const int S=1<<6;
const int inf=0x3f3f3f3f;

int T,n,m,k,vis[N][N],dp[N][N][S],w[N][N],c[N][N],a[N][N],C[N],col[N],cid,fx[4]={1,-1,0,0},fy[4]={0,0,1,-1};
queue<pair<int,int>>q;

inline bool check(int i,int j){return i>=1&&i<=n&&j>=1&&j<=m;}

inline void spfa(int s){
	while(!q.empty()){
		auto t=q.front();q.pop();
		int x=t.first,y=t.second;vis[x][y]=0;
		for(int i=0;i<4;i++){
			int xx=x+fx[i],yy=y+fy[i];
			if(!check(xx,yy)||c[xx][yy]==-1)continue;
			if(dp[xx][yy][s]>dp[x][y][s]+w[xx][yy]){
				dp[xx][yy][s]=dp[x][y][s]+w[xx][yy];
				if(!vis[xx][yy])q.push({xx,yy}),vis[xx][yy]=1;
			}
		}
	}
	return;
}

inline int Steiner(){
	int shuffle=200,ans=inf;
	while(shuffle--){
		random_shuffle(col+1,col+cid+1);
		for(int i=1;i<=cid;i++)C[col[i]]=i%k;
		for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)for(int s=0;s<(1<<k);s++)dp[i][j][s]=inf;
		for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(~c[i][j])dp[i][j][1<<C[c[i][j]]]=w[i][j];
		for(int s=1;s<(1<<k);s++){
			for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(~c[i][j]){
				for(int t=s&(s-1);t;t=(t-1)&s)dp[i][j][s]=min(dp[i][j][s],dp[i][j][t]+dp[i][j][s^t]-w[i][j]);
				if(dp[i][j][s]!=inf)q.push({i,j});
			}
			spfa(s);
		}
		for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)ans=min(ans,dp[i][j][(1<<k)-1]);
	}
	return ans;
}

inline void solve(){
	scanf("%d%d%d",&n,&m,&k),cid=0;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&c[i][j]),col[++cid]=c[i][j];
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]),w[i][j]=1;
	sort(col+1,col+cid+1),cid=unique(col+1,col+cid+1)-col-1;
	int cnt=Steiner();
	if(cnt==inf){puts("-1 -1");return;}
	int l=0,r=1e6,Mid=0;
	while(l<=r){
		int mid=(l+r)>>1;
		for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)w[i][j]=a[i][j]<=mid?9999:10001;
		if(Steiner()<=cnt*10000)r=mid-1,Mid=mid;
		else l=mid+1;
	}
	printf("%d %d\n",cnt,Mid);
	return;
}

int main(){
	scanf("%d",&T);
	while(T--)solve();
	return 0;
}
  • CF482D Random Function and Tree

  • 定义了一种染色 dfs,具体地,将当前点染上访问过的点数的奇偶性,将儿子按照升序/降序随机其中一种排序后遍历,每个点有 12 被访问,求不同染色方案。n105

一个显然的 dp 是抓住奇偶性的特点,fu,0/1 表示 u 子树内染了的点的点数奇偶性为 0/1 的方案数,转移容易。对于将儿子降序遍历相当于 f 乘二即可,但是会有算重的情况。什么情况会在升、降序看来是一样的?对于每个点设 preu,sufi 表示从前往后和从后往前到该点时访问的点数的奇偶性,那么只有 vchildu,preu=sufu 时才会算重,那么显然只可能是每个子树都染了偶数个点,或者是每个子树染了奇数个点且子树个数为奇数。对于这个子问题同样使用 dp:gu,0/1,0/1 表示当前子树是染点数的奇偶性,和之前所有子树都染点数为 0/1,转移也是容易的。注意实现中 g 未算上自己即点 u,所以 fu,0 减去的是 gu,1,1fu,1 同理。

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int mod=1e9+7;
const int N=1e5+6;

int n,f[N][2],g[N][2][2];
vector<int>G[N];

void dfs(int u){
	for(int v:G[u])dfs(v);
	f[u][1]=1,g[u][0][0]=g[u][0][1]=1;
	for(int v:G[u]){
		int t0=(f[u][0]*f[v][0]%mod+f[u][1]*f[v][1]%mod)%mod;
		int t1=(f[u][1]*f[v][0]%mod+f[u][0]*f[v][1]%mod)%mod;
		f[u][0]=(f[u][0]+t0)%mod,f[u][1]=(f[u][1]+t1)%mod;
		int t00=g[u][0][0]*f[v][0]%mod;
		int t01=g[u][1][1]*f[v][1]%mod;
		int t11=g[u][0][1]*f[v][1]%mod;
		g[u][0][0]=(g[u][0][0]+t00)%mod,g[u][0][1]=(g[u][0][1]+t01)%mod,g[u][1][1]=(g[u][1][1]+t11)%mod;
	}
	f[u][0]=(2*f[u][0]%mod-g[u][1][1]+mod)%mod,f[u][1]=(f[u][1]*2%mod-g[u][0][0]+mod)%mod;
	return;
}

signed main(){
	scanf("%lld",&n);
	for(int i=2;i<=n;i++){
		int v=i,u;scanf("%lld",&u);
		G[u].push_back(v);
	}
	dfs(1);
	printf("%lld\n",(f[1][0]+f[1][1])%mod);
    return 0;
}
  • CF1870F Lazy Numbers

  • k 进制下的正整数 1n 按字典序排序得到 p1,p2,,pn, 求满足 px=xx 的数量。T1000,n,k1018

差点不会做呜呜呜。

我们考虑对于所有长度为 l+1 的数 x[kl,kl+1),长度相同的数,其字典序排名也随值上升而增加。所以对于任意两个长度都为 l 的数 x,y(x<y),那么显然有 pypx>0,而题目所求相当于 x[pii=0],所以定义 pi=pii,上式转为 pypx0,所以将 p 按照长度分为 logkn 段函数,每一段都是单调不降的,所以每段函数内 p=0 的一定为一段区间。

这启发我们去枚举长度 l,然后二分区间的左右端点,复杂度为 O(log2Solve(x)),其中 Solve(x) 表示求出 px 的值的复杂度。

处理这个,我们自然地想到对于长度分类:

  • 长度为 l 的数 y 字典序小于等于 x 的有 xkl+1 个。

  • 长度为 i<l 的数 y 字典序小于等于 x 的有 xkliki+1 个。

  • 长度为 j>l 的数 y 字典序小于等于 x 的有 xkjlkj 个。

总时间复杂度 O(Tlog3kn)。注意 n×k 会溢出 long long

#include<bits/stdc++.h>
using namespace std;

#define int long long

int n,k,pw[80],ans,m,T;

inline int calc(int x,int p){
	int res=x-pw[p]+1,t=x;
	for(int i=p-1;i>=0;i--)t/=k,res+=t-pw[i]+1;
	t=x;for(int i=p+1;i<=m;i++){
		if(n/k>=t&&t*k<=n)t=min(t*k,n+1);
		else t=n+1;
		res+=t-pw[i];
	}
	return res;
}

void solve(){
	scanf("%lld%lld",&n,&k);
	pw[m=0]=1,ans=0;
	while(n/k>=pw[m]&&pw[m]*k<=n)pw[m+1]=pw[m]*k,m++;
	for(int i=0;i<=m;i++){
		int l=pw[i],r=(i<m?pw[i+1]-1:n);
		int l1=l,r1=r,L=r+1;
		while(l1<=r1){
			int mid=(l1+r1)>>1;
			if(calc(mid,i)>=mid)L=mid,r1=mid-1;
			else l1=mid+1;
		}
		int l2=l,r2=r,R=l-1;
		while(l2<=r2){
			int mid=(l2+r2)>>1;
			if(calc(mid,i)<=mid)R=mid,l2=mid+1;
			else r2=mid-1;
		}
		if(L<=R)ans+=R-L+1;
	}
	printf("%lld\n",ans);
}

signed main(){
	scanf("%lld",&T);
	while(T--)solve();
	return 0;
}
  • Gym103687E Easy Jump

  • n 个关卡,有些关卡可以回蓝,你初始有生命值 H 和蓝量 S,给定每个关卡通过的概率 pi,在每个关卡,你可以:

  1. 花费 1 单位时间尝试通关,有 px 的概率通过,进入下一关,否则扣除一单位血量并留在当前关。

  2. 花费 T1 单位时间,消耗一单位蓝量,恢复一单位血量,

  3. 花费 T2 单位时间,不消耗蓝量,恢复一单位血量,

  4. 如果关卡 x 是可以回蓝的关卡,则可以恢复任意单位蓝量,不花费时间。

    求按照最优策略保证角色不死的情况下所需的最小期望通关时间。n1000,2H9,S6

H,S 开这么小只是为了契合原题面 Hollow Knight 中的取值:)。

先考虑 S=0T1T2 的情况,此时显然不会选择回蓝。

一个显然的贪心策略是一直尝试通关知道血量为 1 才会选择回血。设 fi,j 表示在第 i 关,血量为 j 的通关概率,转移写出来后从后往前倒退即可。

对于 S>0T1<T2 时会有回蓝操作,如果在不能回蓝的关卡那么转移与之前差不多。考虑在可以回蓝的关卡时,首先我们肯定是直接回满蓝,然后在闯关前先回一定的血量,这个可以直接枚举,因为该关卡任何状态的蓝量都是满的,所以是 O(H) 的。

#include<bits/stdc++.h>
using namespace std;

const int N=1e3+4;
const double inf=1e9;

int n,H,S,T1,T2,m,fl[N];
double p[N],dp[N][7][10];

int main(){
	scanf("%d%d%d",&n,&H,&S);
	for(int i=1;i<=n;i++)scanf("%lf",&p[i]),p[i]/=100;
	scanf("%d",&m);
	for(int i=1,x;i<=m;i++)scanf("%d",&x),fl[x]=1;
	scanf("%d%d",&T1,&T2);
	for(int i=1;i<=n;i++)for(int j=0;j<=S;j++)for(int k=1;k<=H;k++)dp[i][j][k]=inf;
	for(int i=n;i;i--){
		int t=T2;
		if(S&&fl[i])t=min(T1,t);
		for(int j=0;j<=S;j++)for(int k=2;k<=H;k++){
			dp[i][j][k]=min({
				p[i]*dp[i+1][j][k]+(1-p[i])*dp[i][j][k-1]+1,
				(p[i]*dp[i+1][j][k]+(1-p[i])*t+1)/p[i],
				j?p[i]*dp[i+1][j][k]+(1-p[i])*(dp[i][j-1][k]+T1)+1:inf
			});
		}
		if(fl[i]){
			for(int j=S-1;j>=0;j--)for(int k=1;k<=H;k++)dp[i][j][k]=min(dp[i][j][k],dp[i][j+1][k]);
			if(S)for(int j=0;j<=S;j++)for(int k=H-1;k;k--)dp[i][j][k]=min(dp[i][j][k],dp[i][j][k+1]+T1);
		}
	}
	printf("%.9lf\n",dp[1][S][H]);
	return 0;
}
  • P4787 [BalkanOI2018] Minmaxtree

  • 有一棵有 N 个结点的无权树,结点分别编号为 1N。现在要给每条边赋一个权值,使之变为一棵带权树。这棵带权树满足 K 个条件,条件分为两类:

    1.  x y z   从结点 x 到结点 y 的链上最大的边权为 z
    2.  x y z   从结点 x 到结点 y 的链上最小的边权为 z

    保证这 K 组条件的 z 互不相同,构造出这棵树,并输出每条边的边权。n,m7×104

假设我们已知每条边的取值范围 [Li,Ri],不难证明必定存在一种方案使得每条边选择的是 Li 或者 Ri(由于题目保证 Li,Ri 等互不相同)。所以问题就转化成了给每种权值找一条边使得所有权值都出现,直接使用网络流进行二分图匹配 O(nn)

对于求出权值范围可以使用树剖,但是一种码量小的做法是:将权值排序后依次加入,这样每次操作就变成了给路径上还没有染色的边染上一种颜色,直接并查集维护每个点第一个没染色的祖先即可,复杂度 O(nlogn+nα(n))

#include<bits/stdc++.h>
using namespace std;

const int N=7e4+5;
const int inf=1e9;

int n,m,fa[N],dis[N<<1],dep[N],top[N],sz[N],son[N],Fa[N],lsh[N],head[N<<1],cur[N<<1],tot=1,S,T;
vector<int>g[N];
struct node{
	int x,y,v;
	node(int X=0,int Y=0,int V=0){x=X,y=Y,v=V;}
};
vector<node>mx,mn;
struct edge{int to,nxt,w;}e[N<<4];

void addedge(int u,int v,int w){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	e[tot].w=w;
	return;
}

void add(int u,int v){
	addedge(u,v,1),addedge(v,u,0);
	return;
}

void dfs1(int u,int faa){
	dep[u]=dep[fa[u]=faa]+1,sz[u]=1;
	for(int v:g[u])if(v!=faa){
		dfs1(v,u);
		sz[u]+=sz[v];
		if(sz[son[u]]<sz[v])son[u]=v;
	}
	return;
}

void dfs2(int u,int tp){
	top[u]=tp;
	if(son[u])dfs2(son[u],tp);
	for(int v:g[u]){
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
	}
	return;
}

int lca(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])swap(u,v);
		u=fa[top[u]];
	}
	return dep[u]<dep[v]?u:v;
}

bool bfs(){
	queue<int>q;
	memset(dis,0,sizeof(dis)),q.push(S),dis[S]=1;
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;cur[u]=head[u];
			if(!dis[v]&&e[i].w)dis[v]=dis[u]+1,q.push(v);
		}
	}
	return dis[T];
}

int dfs(int u,int flow){
	if(u==T)return flow;
	int res=0;
	for(int&i=cur[u];i;i=e[i].nxt){
		if(!flow)break;
		int v=e[i].to;
		if(e[i].w>0&&dis[v]==dis[u]+1){
			int d=dfs(v,min(e[i].w,flow));
			e[i].w-=d,e[i^1].w+=d,flow-=d,res+=d;
		}
	}
	return res;
}

int find(int x){return Fa[x]!=x?Fa[x]=find(Fa[x]):x;}

int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		g[u].push_back(v),g[v].push_back(u);
	}
	dfs1(1,0),dfs2(1,1);
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		char ch;cin>>ch;
		int x,y,v;scanf("%d%d%d",&x,&y,&v),lsh[i]=v;
		if(ch=='M')mx.push_back(node(x,y,v));
		else mn.push_back(node(x,y,v));
	}sort(lsh+1,lsh+m+1);
	S=n+m+1;T=S+1;
	for(int i=1;i<=n;i++)add(S,i);
	for(int i=1;i<=m;i++)add(i+n,T);
	sort(mx.begin(),mx.end(),[&](node a,node b){return a.v<b.v;});
	for(int i=1;i<=n;i++)Fa[i]=i;
	for(auto t:mx){
		int u=t.x,v=t.y,o=lca(u,v),pos=lower_bound(lsh+1,lsh+m+1,t.v)-lsh,p;
		while(dep[p=find(u)]>dep[o])add(p,pos+n),Fa[p]=fa[p];
		while(dep[p=find(v)]>dep[o])add(p,pos+n),Fa[p]=fa[p];
	}
	sort(mn.begin(),mn.end(),[&](node a,node b){return a.v>b.v;});
	for(int i=1;i<=n;i++)Fa[i]=i;
	for(auto t:mn){
		int u=t.x,v=t.y,o=lca(u,v),pos=lower_bound(lsh+1,lsh+m+1,t.v)-lsh,p;
		while(dep[p=find(u)]>dep[o])add(p,pos+n),Fa[p]=fa[p];
		while(dep[p=find(v)]>dep[o])add(p,pos+n),Fa[p]=fa[p];
	}
	while(bfs())dfs(S,inf);
	for(int u=2;u<=n;u++){
		int res=0;
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if(!e[i].w&&v<=n+m)res=v-n;
		}
		if(!res)for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if(v<=n+m)res=v-n;
		}
		printf("%d %d %d\n",fa[u],u,lsh[res]);
	}
	return 0;
}
  • CF1864H Asterism Stream

  • 给定一变量,初始为 1,每次等概率随机进行以下两种操作之一,令 x 加一或者令 x 乘二。求期望多少次操作之后 xnT 组数据,T100,n1018

官方题解 O(log2n) 很好懂自己看。写的是 amiya 在讨论区提出的一种思路很自然的 O((T+logn)log3n) 的做法。

fi 表示 i 被遍历的期望次数,f 的转移式容易写出,但是 O(n) 的做法不够优秀,注意到前继状态很少,考虑能否使用线性递推或者矩阵表示。我们发现对于 fi,我们只关注 ctz(i),使用向量 Ai 来统计所有 fi/2k

定义矩阵 F 使 fi=Fctz(i)×fi1。那么可求出 Ai=Ai1×Fctz(i)。为了使其能够类似矩阵快速幂,再定义 Gi=2i+11j=1Fj,Hi=2ij=1Fj,那么 G0=H0=F0Hi×Gi1=FiGi=Hi×Gi1

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=60;
const int mod=998244353;
const int inv2=(mod+1)/2;

int T,n;

struct matrix{
	int t[N+1][N+1];
	matrix(){memset(t,0,sizeof(t));}
	matrix operator*(const matrix&rhs)const{
		matrix res;
		for(int i=0;i<=N;i++)
		for(int k=0;k<=i;k++)
		for(int j=0;j<=k;j++)
			res.t[i][j]=(res.t[i][j]+t[i][k]*rhs.t[k][j]%mod)%mod;
		return res;
	}
	matrix operator/(const matrix&rhs)const{
		matrix res;
		for(int k=0;k<=N;k++)
		for(int j=0;j<=k;j++)
			res.t[0][j]=(res.t[0][j]+t[0][k]*rhs.t[k][j]%mod)%mod;
		return res;
	}
}f[N],g[N],h[N],A;

void SolveF(){
	for(int i=0;i<N;i++){
		f[i].t[0][0]=f[i].t[1][0]=1;
		for(int j=0;j<=i;j++){
			int coef=inv2;
			for(int k=j;k<i;k++){
				f[i].t[k+1][j+1]=coef;
				coef=coef*inv2%mod;
			}
			f[i].t[i+1][j+1]=coef;
		}
		 for(int j=i+1;j<N;j++)f[i].t[j+1][j+1]=1;
	}
	return;
}

void SolveG(){
	g[0]=h[0]=f[0];
	for(int i=1;i<N;i++){
		h[i]=g[i-1]*f[i];
		g[i]=h[i]*g[i-1];
	}
	return;
}

void init(){
	for(int i=1;i<=N;i++)A.t[0][i]=2;
	return;
}

void solve(){
	scanf("%lld",&n);
	matrix ans=A;
	for(int i=59;i>=0;i--)if(n&(1ll<<i))ans=ans/h[i];
	printf("%lld\n",(ans.t[0][0]-2+mod)%mod);
	return;
}

signed main(){
	SolveF(),SolveG(),init();
	scanf("%lld",&T);
	while(T--)solve();
	return 0;
}
  • CF1753F Minecraft Series

  • 给定一个 n×m 的矩形,有 k 个人有权值位于其中的坐标上(可能坐标重复),求有多少个选择正方形的方法使选择的正方形中可以选出一个长度 t 的权值连续序列 l,l+1,r1,rl0rn,m4×105,nm4×105,k106

怎么又差点不会。

枚举正方形转为枚举对角线,然后枚举正方形的左上角并双指针维护右下角,而每个人在对于一条对角线的情况中只会被加入一次、删除一次,所以至多被加入 min(n,m)nm。而对于询问则相当于对于正数和负数都做一个 mex 查询。

修改便是 O(knm) 级别,查询 O(nm) 级别,需要平衡二者复杂度,使用值域分块将值域分为 k 段,修改 O(1) 更改对应位的值以及对应段内的信息,查询扫过所有块来找到最小的有缺失元素的段,再在段内暴力查询,O(k)。总时间复杂度 O(knm+nmk)。注意实现卡常,将每个点内权值排序来做到连续内存访问。

#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;

const int N=4e4+5;
const int M=1e6+7;
const int inf=1e9;
const int S=1e3+4;

int n,m,k,lim,cnt[2][M],vis[2][S],anger[M],L,R,block,ans;
vector<vector<int>>a[N];

inline int read(){
	int x=0,fl=1;char ch;ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')fl=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
	return x*fl;
}

inline void write(int x){
	int tmp[11],cnt=0;
	if(!x)putchar('0');
	while(x)tmp[++cnt]=x%10,x/=10;
	while(cnt)putchar(tmp[cnt--]+'0');
	putchar('\n');
}

inline void add(int v){
	if(v<L||v>R)return;
	int o=v>=0;v=abs(v);
	if(!cnt[o][v])vis[o][v/block]++;
	cnt[o][v]++;return;
}

inline void del(int v){
	if(v<L||v>R)return;
	int o=v>=0;v=abs(v);
	if(cnt[o][v]==1)vis[o][v/block]--;
	cnt[o][v]--;return;
}

inline void Add(int x1,int y1,int x2,int y2){
	for(int i=x1;i<=x2;i++)for(int v:a[i][y2])add(v);
	for(int j=y1;j<y2;j++)for(int v:a[x2][j])add(v);
	return;
}

inline void Del(int x1,int y1,int x2,int y2){
	for(int i=x1;i<=x2;i++)for(int v:a[i][y1])del(v);
	for(int j=y1+1;j<=y2;j++)for(int v:a[x1][j])del(v);
	return;
}

inline int qry(){
	int res=0;
	for(int o=0;o<2;o++){
		int k=0;
		while(vis[o][k]==block)k++;
		for(int i=k*block;;i++)if(!cnt[o][i]){
			res+=i;
			break;
		}
	}
	return res-1;
}

int main(){
	n=read(),m=read(),k=read(),lim=read(),block=ceil(sqrt(k));
	for(int i=1;i<=n;i++)a[i].resize(m+1);
	for(int i=1;i<=k;i++){
		int x=read(),y=read(),v=read();
		a[x][y].push_back(v),anger[i]=v;
	}anger[++k]=0;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(a[i][j].size())sort(a[i][j].begin(),a[i][j].end());
	sort(anger+1,anger+k+1),anger[0]=-inf,anger[k+1]=inf;
	int pos=lower_bound(anger+1,anger+k+1,0)-anger;
	L=anger[pos];for(int i=pos;anger[i]-anger[i-1]<2;i--)L=anger[i-1];
	R=anger[pos];for(int i=pos;anger[i+1]-anger[i]<2;i++)R=anger[i+1];
	cnt[0][0]=cnt[1][0]=vis[0][0]=vis[1][0]=1;
	for(int j=1;j<=m;j++){
		int x1=1,y1=j,x2=1,y2=j;
		while(x2<=n&&y2<=m){
			Add(x1,y1,x2,y2);
			while(qry()>=lim&&x1<=x2)Del(x1,y1,x2,y2),x1++,y1++;
			x2++,y2++,ans+=x1-1;
		}x2--,y2--;
		while(x1<=x2)Del(x1,y1,x2,y2),x1++,y1++;
	}
	for(int i=2;i<=n;i++){
		int x1=i,y1=1,x2=i,y2=1;
		while(x2<=n&&y2<=m){
			Add(x1,y1,x2,y2);
			while(qry()>=lim&&x1<=x2)Del(x1,y1,x2,y2),x1++,y1++;
			x2++,y2++,ans+=y1-1;
		}x2--,y2--;
		while(x1<=x2)Del(x1,y1,x2,y2),x1++,y1++;
	}write(ans);
	return 0;
}
  • [AGC029E] Wandering TKHS

  • 给定一棵 n个点的树,你现在在某个点 s。初始时你有集合 S 且只有 s。每次你会选择所以未达到的且与集合中的点有直接连边的点中编号最小的点,并加入集合。当集合中加入 1 立即结束。对于一个 s 定义最终集合大小为 Cs,求 s(1,n],Csn2×105

太能诈骗了。定义 rt=1。自然地拆贡献,我们考虑一点 u 能否被计算进 Cs 里面。令 w=lca(u,s),则 s 必定是先走到 w 才会考虑是否走去 u。那么接下来的过程便与 s 无关。

计算什么样的 v 不能被计算进 w,容易发现当 wrt 的最大值(不包括 w)小于 uw 最大值(不包括 w)时显然不会走到 u

你发现这对于 v 的限制很松,再拆贡献,转化为怎么样的 w 能使得 u 作出贡献。那么设 mx 表示 urt 上最大值的点,mx 下方的 w 显然必定经过 uw 上方的必定不经过 u。而对于 w=mx 的情况再记录一个 se 表示urt 上最次值的点,判断 mx,se 的深度关系即可。那么合法的 w 关于 u 的合法点集便是一条路径,那么对应回去的 s 便是一个子树。树上差分。O(n)

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+6;

int n,tot,head[N],t[N],mx[N],se[N],son[N],dep[N];
struct edge{int to,nxt,w;}e[N<<1];

void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

void dfs(int u,int fa){
	dep[u]=dep[fa]+1;
	if(u>mx[u])se[u]=mx[u],mx[u]=u;
	else se[u]=max(se[u],u);
	if(u^1)t[dep[se[u]]<dep[mx[u]]?mx[u]:son[mx[u]]]++;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		son[u]=v,mx[v]=mx[u],se[v]=se[u],dfs(v,u);
	}
	return;
}

void Dfs(int u,int fa){
	t[u]+=t[fa];
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		Dfs(v,u);
	}
	return;
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(u,v),addedge(v,u);
	}
	dfs(1,0),Dfs(1,0);
	for(int i=2;i<=n;i++)printf("%d ",t[i]);
	return 0;
}
  • [AGC016E] Poor Turkeys

  • 共有 n 只火鸡,并给出了 m 对关系,形如两只火鸡,将按照给出关系的顺序,对于每一对火鸡随机选择一个吃掉。被吃掉的火鸡就死了。两只都被吃了就不会操作。说白了就是一只火鸡只能被吃一次。求有多少对 (i,j) 表示 i,j 这两只火鸡能活下来。n400

先时间倒流。考虑钦定活下来一只的情况,设它是 x,称它为被保护的。那么对于 x 所有所在的关系,另一只火鸡必须保证在它与 x 所在关系之前不能被吃。那么相当于这些火鸡也是被保护的。

所以对于一只最终存活的火鸡,会形成一个集合 S 保证其中的火鸡都是被保护的。什么情况下会不合法?当有一组关系中的两只火鸡都在集合中就寄了。

两只鸡同理。一只鸡只能死一次。所以看 SiSj 是否有交即可。你可以用 bitset 优化,反正数据范围小。

#include<bits/stdc++.h>
using namespace std;

const int N=420;
const int M=1e5+6;

int n,m,x[M],y[M],ans,fl[N];
bitset<N>S[N];

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)scanf("%d%d",&x[i],&y[i]);
	for(int i=1;i<=n;i++){
		S[i].set(i);
		for(int j=m;j;j--){
			int u=S[i][x[j]],v=S[i][y[j]];
			if(u&&v){fl[i]=1;break;}
			if(u)S[i].set(y[j]);
			if(v)S[i].set(x[j]);
		}
	}
	for(int i=1;i<n;i++)if(!fl[i]){
		for(int j=i+1;j<=n;j++)if(!fl[j]){
			bool fl=1;
			for(int k=1;k<=n;k++)if(S[i][k]&&S[j][k]){fl=0;break;}
			ans+=fl;
		}
	}
	printf("%d",ans);
	return 0;
}
  • CF1767E Algebra Flash

  • 有一个长度为 n 的序列,你需要从 1 开始走到 n。每一次可以向前 12 步。每个格子有个颜色 ai,共 m 种,每一种颜色有花费 xi,你需要买下你经过的所有颜色。求最小花费。1n3×105,1m40

m40 显然类似与折半然后状压 dp。

将题目用限制写出来:a1,an 必选,任意两个相邻点的颜色必选。

那么这很像最小点覆盖,将 a1a1,anan,i,aiai+1。那么这确实就是最小点覆盖。最小点覆盖=权值和-最大权独立集。所以转为求最大独立集。使用状压 dp 然后用 map 记忆化搜索即可。具体即设 dpS 表示集合 S 中的最大独立集权值和。

所以复杂度为什么是对的呢?我们考虑优化 dp 转移,每次考虑删除当前编号最小的点。那么,当 lowbit(S)<m/2 时,从全集转移到 S 的过程必定是只减去了 <lowbit(S) 的位置,那么状态只有 <2m/2 种。否则意味着 [0,m/2) 都被选择,对于 [m/2,m] 也同样有 2m/2 种状态。所以复杂度是对的。O(n+m2m/2)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=42;
const int M=3e5+6;

int n,m,col[M],val[N],g[N],ans;
map<int,int>dp;

void add(int u,int v){
	g[u]|=1ll<<v;
	g[v]|=1ll<<u;
}

int dfs(int s){
	if(dp.count(s))return dp[s];
	int x=log2(s&-s);
	int res=dfs(s^(1ll<<x));
	if(!(g[x]&(1ll<<x)))res=max(res,dfs(s^(1ll<<x)^(s&(g[x])))+val[x]);
	return dp[s]=res;
}

signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",&col[i]),col[i]--;
	for(int i=2;i<=n;i++)add(col[i-1],col[i]);
	add(col[1],col[1]),add(col[n],col[n]);
	for(int i=0;i<m;i++)scanf("%lld",&val[i]),ans+=val[i];
	dp[0]=0,ans-=dfs((1ll<<m)-1);
	printf("%lld",ans);
	return 0;
}
  • loj6062. 「2017 山东一轮集训 Day2」Pair

  • 给出一个长度为 n 的数列 ai 和一个长度为 m 的数列 bi,求 ai 有多少个长度为 m 的连续子序列能与 bi 匹配。两个数列可以匹配,当且仅当存在一种方案,使两个数列中的数可以两两配对,两个数可以配对当且仅当它们的和不小于 hn,m1.5×105

先将 b 排序,将 ai 看作是 hai,那么配对相当于 aibi,故每个 ai 可以对应的 bi 是一段后缀。根据霍尔定理得一段连续子序列合法当且仅当对于每个后缀有 sufii,其中 sufi 表示 aii 开始的这段后缀能够对应的 b 后缀之和。

所以使用线段树维护,初始设 bi=i,每次加入一个 ai 就给 [ai,m] 加一,一段区间是否合法便查询全局最大值是否 0O(nlogn)

#include<bits/stdc++.h>
using namespace std;

#define ls p<<1
#define rs p<<1|1

const int N=1.5e5+6;

int n,m,h,tr[N<<2],tag[N<<2],a[N],b[N],ans;

void pushup(int p){
	tr[p]=min(tr[ls],tr[rs]);
	return;
}

void pushdown(int p){
	if(!tag[p])return;
	tr[ls]+=tag[p],tr[rs]+=tag[p],tag[ls]+=tag[p],tag[rs]+=tag[p];
	tag[p]=0;return;
}

void build(int p,int l,int r){
	if(l==r){tr[p]=-l;return;}
	int mid=(l+r)>>1;
	build(ls,l,mid),build(rs,mid+1,r);
	pushup(p);
	return;
}

void update(int p,int l,int r,int s,int t,int val){
	if(s>t)return;
	if(s<=l&&r<=t){
		tr[p]+=val,tag[p]+=val;
		return;
	}
	int mid=(l+r)>>1;
	pushdown(p);
	if(s<=mid)update(ls,l,mid,s,t,val);
	if(t>mid)update(rs,mid+1,r,s,t,val);
	pushup(p);
	return;
}

int main(){
	scanf("%d%d%d",&n,&m,&h);
	for(int i=1;i<=m;i++)scanf("%d",&b[i]);
	sort(b+1,b+m+1),build(1,1,m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),a[i]=lower_bound(b+1,b+m+1,h-a[i])-b;
	for(int i=1;i<m;i++)update(1,1,m,a[i],m,1);
	for(int i=m;i<=n;i++){
		if(i>m)update(1,1,m,a[i-m],m,-1);
		update(1,1,m,a[i],m,1),ans+=(tr[1]>=0);
	}
	printf("%d",ans);
	return 0;
}
  • CF1879F Last Man Standing

  • 给定 n 个人,每个人有两个属性 hi,ai。定义一个难度为 x 的轮为:每回合对于所有人将当前的 a 减去 x,对于 a 被减到 0 的所有人,其 a 将恢复到 ai,且 hi 减一。当一个人的 hi0 时推出该轮。此轮游戏将一直回合下去直到剩下仅剩一人,则此人获得积分,积分为该选手作为仅剩的一人的轮数。如最后局面剩下多个人并在同一回合中一起退出,则所有人都无积分。现在 x>0 都会运行一次难度为 x 的轮,轮与轮之间相互独立,求每个人可以获得最多的积分是多少。n,hi,ai2×105

还是建议看英文原题面吧。

对于一个 x,显然每个选手会在第 k=ax×h 回合退出。而最终存活下来的选手即 k 最大的选手,并获得 kmxkse 的积分,即存货最久的人的轮数减去第二久的人的轮数。

注意到 h,a 值域很小,可以枚举 x,然后对于每种 x 都计算出当前的答案。ax 启发我们对于 c=ax 的取值进行分类,那么对于 ai[(c1)x+1,cx] 的所有人我们只关心其中 h 的最大值与次大值,因为我们只关系全局上的最大值和次大值。

如果我们知道每段区间的 hmx,hse,那么我们可以暴力便利每个 c 的区间然后得出最大与次大值,在 O(AlogA) 的时间复杂度内完成。

若只查询最大值便是模板的 ST 表,对于次大值的维护同样可以使用 ST 表,我们记录最大值出现位置的极左和极右从而判断当前的 se 能否取到 mx。更具体见 ST表维护区间次大值。总复杂度 O(n+AlogA)

#include<bits/stdc++.h>
using namespace std;

#define ll long long
#define mem(x) memset(x,0,sizeof(x))

const int N=2e5+6;
const int K=20;

int T,h[N],a[N],n,L[K][N],R[K][N],mx[K][N],se[K][N],edpos[N],lg[N];
ll ans[N],edmx[N],edse[N];

int qrymx(int l,int r){
	int k=lg[r-l+1];
	return max(mx[k][l],mx[k][r-(1<<k)+1]);
}

int qryse(int l,int r){
	int k=lg[r-l+1];
	if(mx[k][l]==mx[k][r-(1<<k)+1]&&L[k][l]!=R[k][r-(1<<k)+1])return mx[k][l];
	else if(mx[k][l]>mx[k][r-(1<<k)+1])return max(se[k][l],mx[k][r-(1<<k)+1]);
	else if(mx[k][r-(1<<k)+1]>mx[k][l])return max(se[k][r-(1<<k)+1],mx[k][l]);
	else return max(se[k][l],se[k][r-(1<<k)+1]);
}

int qrypos(int l,int r){
	int k=lg[r-l+1];
	if(mx[k][l]>=mx[k][r-(1<<k)+1])return L[k][l];
	else return R[k][r-(1<<k)+1];
}

void solve(){
	scanf("%d",&n);int A=0;
	mem(ans),mem(L),mem(R),mem(mx),mem(se),mem(edpos),mem(edmx),mem(edse);
	for(int i=1;i<=n;i++)scanf("%d",&h[i]);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),A=max(A,a[i]);
	for(int i=1;i<=n;i++){
		if(mx[0][a[i]]<h[i]){
			se[0][a[i]]=mx[0][a[i]],mx[0][a[i]]=h[i];
			L[0][a[i]]=R[0][a[i]]=i;
		}
		else if(se[0][a[i]]<h[i])se[0][a[i]]=h[i];
	}
	for(int k=1;k<=lg[A]+1;k++)for(int i=1;i+(1<<k)-1<=A;i++){
		int j=i+(1<<k-1),lmx=mx[k-1][i],rmx=mx[k-1][j];
		mx[k][i]=max(lmx,rmx);
		if(lmx==rmx)L[k][i]=L[k-1][i],R[k][i]=R[k-1][j];
		else if(lmx>rmx)L[k][i]=L[k-1][i],R[k][i]=R[k-1][i];
		else L[k][i]=L[k-1][j],R[k][i]=R[k-1][j];
		se[k][i]=max(se[k-1][i],se[k-1][j]);
		if(lmx!=rmx)se[k][i]=max(se[k][i],min(lmx,rmx));
		if(lmx==rmx&&L[k][i]!=R[k][i])se[k][i]=mx[k][i];
	}
	for(int x=1;x<=A;x++){
		for(int l=1;l<=A;l+=x){
			int r=min(A,l+x-1),c=(l-1)/x+1,pos=qrypos(l,r);
			ll Mx=1ll*qrymx(l,r)*c,Se=1ll*qryse(l,r)*c;
			if(Mx>edmx[x])edse[x]=edmx[x],edmx[x]=Mx,edpos[x]=pos;
			else if(Mx>edse[x])edse[x]=Mx;
			edse[x]=max(edse[x],Se);
		}
		ans[edpos[x]]=max(ans[edpos[x]],edmx[x]-edse[x]);
	}
	for(int i=1;i<=n;i++)printf("%lld ",ans[i]);puts("");
	return;
}

int main(){
	scanf("%d",&T);
	for(int i=2;i<N;i++)lg[i]=lg[i>>1]+1;
	while(T--)solve();
	return 0;
}
  • CF1221F Choose a Square

  • 在平面直角坐标系上给定 n 个带权值的点,求权值最大的正方形且左下角与右上角都在 y=x 直线上。定义一个正方形的权值为正方形边上及内部所有点权值之和减去正方形边长。n5×105

好蠢。考虑一个点怎么样会被包含进一个正方形内,或者说,哪些正方形会包含它?显然,设正方形左下角是 (l,l),右上角 (r,r),那么所有 lx,yr 的点 (x,y) 都在其中。

min(x,y)l,max(x,y)r,那么每个点就转为一段区间 [min(x,y),max(x,y)],问题便是求一段区间使得该区间覆盖线段权值和最大。至于边长 rl 初始时在线段树上赋值为 l 即可。O(nlogn)

#include<bits/stdc++.h>
using namespace std;

#define ls p<<1
#define rs p<<1|1
#define int long long

const int N=1e6+7;
const int inf=0x3f3f3f3f3f3f3f3f;

int n,m,tr[N<<2],tag[N<<2],pos[N<<2],tot,lsh[N],L,R,ans=-inf;
vector<pair<int,int>>q[N];
struct node{int l,r,w;}a[N];

void pushup(int p){
	tr[p]=max(tr[ls],tr[rs]);
	if(tr[p]==tr[ls])pos[p]=pos[ls];
	else pos[p]=pos[rs];
	return;
}

void pushdown(int p){
	if(!tag[p])return;
	tag[ls]+=tag[p],tag[rs]+=tag[p],tr[ls]+=tag[p],tr[rs]+=tag[p];
	tag[p]=0;return;
}

void build(int p,int l,int r){
	if(l==r){
		tr[p]=-lsh[l],pos[p]=l;
		return;
	}
	int mid=(l+r)>>1;
	build(ls,l,mid),build(rs,mid+1,r);
	pushup(p);
	return;
}

void update(int p,int l,int r,int s,int t,int val){
	if(s<=l&&r<=t){
		tr[p]+=val,tag[p]+=val;
		return;
	}
	int mid=(l+r)>>1;
	pushdown(p);
	if(s<=mid)update(ls,l,mid,s,t,val);
	if(t>mid)update(rs,mid+1,r,s,t,val);
	pushup(p);
	return;
}

auto query(int p,int l,int r,int s,int t){
	if(s<=l&&r<=t)return make_pair(tr[p],pos[p]);
	int mid=(l+r)>>1;
	pushdown(p);
	if(t<=mid)return query(ls,l,mid,s,t);
	if(s>mid)return query(rs,mid+1,r,s,t);
	auto lans=query(ls,l,mid,s,t),rans=query(rs,mid+1,r,s,t);
	if(lans.first>rans.first)return lans;
	else return rans;
}

signed main(){
	scanf("%lld",&n),memset(tr,-0x3f,sizeof(tr));
	for(int i=1;i<=n;i++){
		int x,y,w;
		scanf("%lld%lld%lld",&x,&y,&w);
		lsh[++tot]=x,lsh[++tot]=y;
		a[i]=(node){min(x,y),max(x,y),w};
	}sort(lsh+1,lsh+tot+1),tot=unique(lsh+1,lsh+tot+1)-lsh-1;
	for(int i=1;i<=n;i++){
		a[i].l=lower_bound(lsh+1,lsh+tot+1,a[i].l)-lsh;
		a[i].r=lower_bound(lsh+1,lsh+tot+1,a[i].r)-lsh;
		q[a[i].l].push_back(make_pair(a[i].r,a[i].w)),m=max(m,a[i].r);
	}
	build(1,1,m);
	for(int i=m;i;i--){
		for(auto t:q[i])update(1,1,m,t.first,m,t.second);
		auto t=query(1,1,m,i,m);t.first+=lsh[i];
		if(t.first>ans)ans=t.first,L=lsh[i],R=lsh[t.second];
	}
	if(ans<0)ans=0,L=lsh[m]+1,R=lsh[m]+1;
	printf("%lld\n%lld %lld %lld %lld",ans,L,L,R,R);
	return 0;
}
  • Ptz Winter 2021 Day 5 C. Multiple?

  • 给定 n,k,求出长度为 nk 的值域在 [1,n] 且所有非空连续子段和都不是 n 的倍数的整数数列 a 的数量。k<n/4<n<998244353

子序列的和很难刻画,我们考虑限制更松的情况:子段的和。

自然地想到前缀和来刻画子段和。我们一开始有了 nk+1 个前缀和(包括前缀 0),那么考虑交换两个数,我们能构造多少不同前缀和?是 nk+1+m 个,其中 m 表示我最多能将原序列分成多少组使得每组内两个数且不同。那么 m=min(nk2,nkt),其中 t 表示众数 x 出现次数。根据抽屉原理有 nk+1+m<n,所以 m<k<n/4,易得 t>n2

注意到若 gcd(n,x)>1 则必定可以找到 n/gcd(n,x)x 使它们模 n0,所以 gcd(n,x)=1,那么 x 就有逆元,将所有数乘上 x1,此时就有了 t1。而现在与原题目限制之间还差一个条件:所有数之和 <n,若满足以上所有条件便是充要的。

S=a>n 则剩下的数和 St>nt 必定存在可以和 t1 配对凑出 n 的数;对于充分性显然。

所以原题目便转变为要求至少 n/21,总和 <n 的有序序列数,使用插板法得 (n1k1)。再乘上 xϕ(n) 种取值。时间复杂度 O(k)

#include <bits/stdc++.h>
using namespace std;

#define int long long

const int mod=998244353;

int n,k,fac=1,ifac=1,nn,phi=1;

int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

signed main(){
	scanf("%lld%lld",&n,&k),nn=n;
	for(int i=2;i*i<=n;i++)if(n%i==0){
		phi=phi*(i-1),n/=i;
		while(n%i==0)n/=i,phi*=i;
	}if(n>1)phi=phi*(n-1);n=nn;
	for(int i=n-k+1;i<n;i++)fac=fac*i%mod;
	for(int i=1;i<k;i++)ifac=ifac*i%mod;ifac=qpow(ifac,mod-2);
	printf("%lld",phi*fac%mod*ifac%mod);
	return 0;
}
  • CF1246F Cursor Distance

  • 给定一个长度为 n 的由小写字母组成的字符串 s,当光标在 x 位置时,一次合法的操作定义为选取 (c,0/1) 分别表示一个字符和方向(左/右),执行该操作会使光标移动到 x 对应方向的 c 字符的最接近的的出现位置(要求必须存在)。定义 dis(i,j) 为光标初始在 i ,使光标移动到 j 的最小操作次数。求 ni=1nj=1dis(i,j)n105

我们考虑刻画 dis(i,j) 这个东西。从 i 开始跳 j 步所能跳到的点不一定形成一个区间,正难则反,发现跳 j 步到 i 的点集是一个区间,我们用 [li,j,ri,j] 来刻画。

首先答案可以被改写成 ni=1kn1(ri,kli,k),那么转化为快速求出 l,r,一个自然的想法是倍增。然而,写出式子:[li,k+1,ri,k+1]=li,kjri,k[lj,0,rj,0]

看起来 l,r 是互相影响的而不能倍增,我们进行更仔细的观察:设 [li,k,ri,k] 中每种字符首次出现位置从小到大排序后是 pos1,pos2,posm,那么 li,k+1minlposj,0 更新。格式化地:li,k+1=minj26lposj,0,其中对于 [li,k,ri,k] 中未出现的字符设为 n,注意 pos 仍然是按照从小到大排序的。

那么,若当前字符集维护的 pos 不变,那么 l,r 各自独立,便可以各自倍增得出 lk,rk。再注意到我们关心的只是区间中字符集大小而不是具体的集合,那么字符集大小 |Σ| 的变化量只有 26,我们枚举 |Σ| 的大小分别算出此时的 li,k,ri,k,判断区间是否具有 |Σ| 个字符即可。复杂度 O(26nlogn)

#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=1e5+6;

int n,l[N],r[N],L[N][27],R[N][27],pre[N][27],nxt[N][27],lst[27],f[18][N],g[18][N],F[18][N],G[18][N],ans;
char s[N];

signed main(){
	scanf("%s",s+1),n=strlen(s+1);
	for(int i=0;i<26;i++)lst[i]=1;
	for(int i=1;i<=n;i++){
		l[i]=lst[s[i]-'a'],lst[s[i]-'a']=i;
		memcpy(pre[i],lst,sizeof(pre[i])),sort(pre[i],pre[i]+26,greater<int>());
	}for(int i=0;i<26;i++)lst[i]=n;
	for(int i=n;i>=1;i--){
		r[i]=lst[s[i]-'a'],lst[s[i]-'a']=i;
		memcpy(nxt[i],lst,sizeof(nxt[i])),sort(nxt[i],nxt[i]+26),nxt[i][26]=n+1;
	}
	for(int i=1;i<=n;i++){
		L[i][0]=R[i][0]=i;
		for(int j=0;j<26;j++)L[i][j+1]=min(L[i][j],l[nxt[i][j]]),R[i][j+1]=max(R[i][j],r[pre[i][j]]);
	}
	for(int i=1;i<=n;i++)l[i]=r[i]=i;
	for(int t=1;t<=26;t++){
		for(int i=1;i<=n;i++)f[0][i]=L[i][t],g[0][i]=R[i][t],F[0][i]=G[0][i]=i;
		for(int j=1;(1<<j)<=n;j++)for(int i=1;i<=n;i++){
			f[j][i]=f[j-1][f[j-1][i]],g[j][i]=g[j-1][g[j-1][i]];
			F[j][i]=F[j-1][i]+F[j-1][f[j-1][i]],G[j][i]=G[j-1][i]+G[j-1][g[j-1][i]];
		}
		for(int i=1;i<=n;i++){
			for(int j=31-__builtin_clz(n);j>=0;j--){
				int nl=f[j][l[i]],nr=g[j][r[i]];
				if(nr<nxt[nl][t]){
					ans+=((n-1)<<j)-(G[j][r[i]]-F[j][l[i]]);
					l[i]=nl,r[i]=nr;
				}
			}
			if(r[i]<nxt[l[i]][t]){
				ans+=n-1-(r[i]-l[i]);
				l[i]=L[l[i]][t],r[i]=R[r[i]][t];
			}
		}
	}
	printf("%lld",ans);
	return 0;
}
  • CF1383C String Transformation 2

  • 给定两个字符串 AB,每次可以选取 A若干个相同的字符 x,然后将其改成 y,求使得 A=B 的最小操作次数。|A|,|B|105AB 仅由 a t 构成(共计 20 种字符)。

删去已经相同的位置,然后每对 ai,bi,那么看作 aibi 连了一条有向边,显然每个弱连通块独立,所以对于每个弱连通块讨论。

对于一个 DAG,答案为点数减一。考虑怎么构造:对于 DAG 来分层,对于跨层的边实际上没有用所以可以删掉,那么每一层的点之间连边,然后上一层最后一个点联想这一层第一个点,不难发现这也是最小的方式,因为形成的就是一棵生成树。

对于有环的图,我们可以花两步减少一个点,具体地便是假如要删掉 u,第一步是选一个 v,把 u 变成 v,最后一步是把 v 变成 u。这样连向 u 的边都可以看做连向 v 的,u 连出去的点都可以看做 v 连出去的。

所以我们可以一直删点只剩下一个 DAG,因为 DAG 的答案是确定且最优的;那么我们肯定是剩下最大的点集 S 使得 S 的导出子图是 DAG。所以答案的一个下界就是 (n|S|)×2+(|S|1)=2n|S|1

事实上这也是最优解。证明我们先考虑将结论转化为:对于以个弱连通块可以用 k 步解决,那么一定存在点集 S 使得 2nk1|S|S 导出子图为 DAG。

考虑维护 S 和所有 DAG 的和 T,初始时每个点自身就是一个 DAG,考虑一直加边,当加入 (u,v)

  1. (u,v) 在同一个连通块则 vS 中删去且 T 减一,连通块数 c 不变。
  2. 若不在一个连通块,则 T 不变且 c 减一。

那么令该连通块点数为 n,其中最多 n1 步删连通块数 c,所以至多 k(n1) 次减 T,那么 n(k(n1))|S|2nk1|S|

所以结论成立,答案就是 20×2|S|c,找到最大的点集 S 使得 S 的导出子图是 DAG 使用状压 dp 即可。复杂度 O(n+220×20)

还有这题*3100评紫/kx

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+6;

int T,n,fa[20],g[20],dp[1<<20],ans,sz;
char s[N],t[N];

int find(int x){return fa[x]^x?fa[x]=find(fa[x]):x;}

void solve(){
	scanf("%d%s%s",&n,s,t),sz=0;
	for(int i=0;i<20;i++)fa[i]=i,g[i]=0;
	for(int i=0;i<(1<<20);i++)dp[i]=0;
	for(int i=0;i<n;i++){
		g[s[i]-'a']|=1<<(t[i]-'a');
		fa[find(s[i]-'a')]=find(t[i]-'a');
	}
	dp[0]=1,ans=40;
	for(int i=0;i<20;i++)if(i==find(i))ans--;
	for(int s=0;s<(1<<20);s++)if(dp[s]){
		for(int i=0;i<20;i++)if(!(s&(1<<i))&&!(s&g[i]))dp[s|(1<<i)]=1;
		sz=max(sz,__builtin_popcount(s));
	}
	printf("%d\n",ans-sz);
	return;
}

int main(){
	scanf("%d",&T);
	while(T--)solve();
	return 0;
}
  • PTZ summer 2021 Day3 I. Intellectual Implementation

  • 给定 n 个长方形 Ti,问有几个三元组 i<j<k 满足 Ti,Tj,Tk 两两无交。n2×105。保证长方形的 l,r,u,d 两两不同。

容斥,用 (n3) 减去不合法的方案。对于不合法的情况分类:

  1. 两个有交且都和另一个无交
  2. 两个有交其中一个和剩下一个有交另一个无交
  3. 两两有交

将上述三种情况的方案数分别设为 c1,c2,c3,设 cnti 表示和第 i 个矩形有交的矩形个数,那么我们有:

icnti(n2)=2c1+4c2+6c3

i(cnti2)=c2+3c3

所以我们可以直接得出 c1+c2=cnti(n1cnti)2,至于对于 cnti 的计算使用线段树即可。

那么考虑计算 c3,枚举三个矩形中 ui 最小的,剩下的矩形要保证和它有交等价于 dj<ui<uj,那么刻画为在 dj 插入 [lj,rj],在 uj 删去,然后查询区间中有多少 j,k 使得 [li,ri],[lj,rj],[lk,rk] 两两有交,这就把问题拍到一维上了。

那么计算这个再次容斥,变为选择两个区间有交的方案数减去和 [li,ri] 有交而这两个区间无交的方案数。前者容易计算,后者等价于 li<rj<lk<ri,那么使用线段树维护每个区间内有多少个区间左/右端点即可。复杂度 O(nlogn)

#include<bits/stdc++.h>
using namespace std;

#define ls p<<1
#define rs p<<1|1
#define ll long long

const int N=2e5+6;

int n,tot,cnt[N],lsh[N<<1];
ll ans;
struct node{int x[4],i;}a[N],b[N];

namespace AddCnt{
	int tr[N<<1];
	int lowbit(int x){return x&-x;}
	void add(int x){while(x<=tot)tr[x]++,x+=lowbit(x);return;}
	int qry(int x){int res=0;while(x)res+=tr[x],x-=lowbit(x);return res;}
	void clear(){memset(tr,0,sizeof(tr));return;}
	void solve(int p){
		if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1;
		sort(a+1,a+n+1,[&](node x,node y){return x.x[p]<y.x[p];});
		for(int i=1;i<=n;i++){
			cnt[a[i].i]+=qry(a[i].x[p]);
			add(a[i].x[p^1]);
		}
		if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1;
		clear();return;
	}
}

struct Node{
	int l,r;ll t;
	Node operator+(const Node&rhs)const{return{l+rhs.l,r+rhs.r,t+rhs.t+1ll*r*rhs.l};}
}tr[N<<3];

void pushup(int p){
	tr[p]=tr[ls]+tr[rs];
	return;
}

void build(int p,int l,int r){
	tr[p].t=0ll,tr[p].l=tr[p].r=0;
	if(l==r)return;
	int mid=(l+r)>>1;
	build(ls,l,mid),build(rs,mid+1,r);
	return;
}

void addL(int p,int l,int r,int ps,int val){
	if(l==r){tr[p].l+=val;return;}
	int mid=(l+r)>>1;
	if(ps<=mid)addL(ls,l,mid,ps,val);
	else addL(rs,mid+1,r,ps,val);
	pushup(p);
	return;
}

void addR(int p,int l,int r,int ps,int val){
	if(l==r){tr[p].r+=val;return;}
	int mid=(l+r)>>1;
	if(ps<=mid)addR(ls,l,mid,ps,val);
	else addR(rs,mid+1,r,ps,val);
	pushup(p);
	return;
}

auto qry(int p,int l,int r,int s,int t){
	if(s<=l&&r<=t)return tr[p];
	int mid=(l+r)>>1;
	if(t<=mid)return qry(ls,l,mid,s,t);
	if(s>mid)return qry(rs,mid+1,r,s,t);
	return qry(ls,l,mid,s,t)+qry(rs,mid+1,r,s,t);
}

namespace SubCnt{
	void solve(int p){
		if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1;
		build(1,1,tot);
		for(int i=1;i<=n;i++)b[i]=a[i];
		sort(a+1,a+n+1,[&](node x,node y){return x.x[p]<y.x[p];});
		sort(b+1,b+n+1,[&](node x,node y){return x.x[p^1]<y.x[p^1];});
		for(int i=1,j=1;i<=n;i++){
			while(j<=n&&b[j].x[p^1]<a[i].x[p])addL(1,1,tot,b[j].x[2],1),addR(1,1,tot,b[j].x[3],1),j++;
			cnt[a[i].i]-=qry(1,1,tot,1,a[i].x[2]).r+qry(1,1,tot,a[i].x[3],tot).l;
		}
		if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1;
		return;
	}
}

void C3(){
	build(1,1,tot);
	for(int i=1;i<=n;i++)b[i]=a[i];
	sort(a+1,a+n+1,[&](node x,node y){return x.x[0]<y.x[0];});
	sort(b+1,b+n+1,[&](node x,node y){return x.x[1]<y.x[1];});
	for(int i=1,j=1;i<=n;i++){
		while(j<=n&&b[j].x[1]<a[i].x[0])addL(1,1,tot,b[j].x[2],-1),addR(1,1,tot,b[j].x[3],-1),j++;
		int t=tr[1].l-qry(1,1,tot,1,a[i].x[2]).r-qry(1,1,tot,a[i].x[3],tot).l;
		ans+=t*(t-1ll)/2ll-qry(1,1,tot,a[i].x[2],a[i].x[3]).t;
		addL(1,1,tot,a[i].x[2],1),addR(1,1,tot,a[i].x[3],1);
	}
	return;
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		for(int j=0;j<4;j++)scanf("%d",&a[i].x[j]);
		a[i].i=i;
	}
	for(int j=0;j<4;j+=2){
		tot=0;
		for(int i=1;i<=n;i++)lsh[++tot]=a[i].x[j],lsh[++tot]=a[i].x[j+1];
		sort(lsh+1,lsh+tot+1);
		for(int i=1;i<=n;i++)a[i].x[j]=lower_bound(lsh+1,lsh+tot+1,a[i].x[j])-lsh,a[i].x[j+1]=lower_bound(lsh+1,lsh+tot+1,a[i].x[j+1])-lsh;
	}
	for(int p=0;p<4;p++)AddCnt::solve(p);
	SubCnt::solve(0),SubCnt::solve(1);
	for(int i=1;i<=n;i++)ans+=cnt[i]*(n-1ll-cnt[i]);
	ans/=2ll,C3();
	printf("%lld",1ll*n*(n-1)*(n-2)/6-ans);
	return 0;
}
  • [ARC093F] Dark Horse

  • 2n 个人进行比赛,以满二叉树的方式进行淘汰赛。你是编号 1,你只打不过 a1<a2,am 编号的人,对于剩下的你都打得过。对于没有你参与的一餐比赛胜负定义编号小的人赢。求所有 (2n)! 种位置的排列中你赢的情况之和。n,m16

首先你的位置不重要,不妨设为 1 然后最后答案乘上 2n

那么这个淘汰赛中你会参与的区间便是形如线段树的节点:k,[2k1+1,2k]。那么你会输当且仅当这 n 个区间中有一个区间的最小值为 a 中的人。

直接计算不太行,自然地进行容斥,设 fi,s 表示考虑了 a1ais 是二进制下的状态表示第 k 个区间的最小值已经被钦定为 a1ai 中的某个值了。转移显然枚举 ai+1,但是由于 aiai+1 中不知道有多少数使用过了,难以转移。

注意我们只关心比当前 ai 小的值,所以将 a reverse 后再进行上述 dp 即可。转移时候,所有被填入 s 中的区间的数都是 ai 的数,不会影响 ai。枚举当前的 a 放进哪个长度为 2k 的区间,填充这个区间的数便在 (ai,2n] 中选择,注意要减去被选择了的 s 个数,所以转移系数便是 (2nais2k1)×(2k)!,时间复杂度 O(n22n)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=18;
const int S=1<<17;
const int mod=1e9+7;

int n,m,a[N],fac[S],ifac[S],pw[N],dp[N][S],ans;

int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

void init(){
	fac[0]=ifac[0]=pw[0]=1;
	for(int i=1;i<S;i++)fac[i]=fac[i-1]*i%mod;
	for(int i=1;i<N;i++)pw[i]=pw[i-1]*2%mod;
	ifac[S-1]=qpow(fac[S-1],mod-2);
	for(int i=S-2;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
	return;
}

int C(int n,int m){
	if(n<m||n<0||m<0)return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

signed main(){
	scanf("%lld%lld",&n,&m),init();
	for(int i=1;i<=m;i++)scanf("%lld",&a[i]);
	reverse(a+1,a+m+1),dp[0][0]=1;
	for(int i=0;i<m;i++)for(int s=0;s<pw[n];s++){
		for(int k=0;k<n;k++)if(!(s&(1<<k)))
			dp[i+1][s|(1<<k)]=(dp[i+1][s|(1<<k)]+dp[i][s]*C(pw[n]-a[i+1]-s,pw[k]-1)%mod*fac[pw[k]]%mod)%mod;
		dp[i+1][s]=(dp[i+1][s]+dp[i][s])%mod;
	}
	for(int s=0;s<pw[n];s++){
		int fl=__builtin_popcount(s)&1;
		ans=(ans+(fl?-1:1)*dp[m][s]*fac[pw[n]-1-s]%mod+mod)%mod;
	}
	printf("%lld",ans*pw[n]%mod);
	return 0;
}
  • AtCoder Code Festival 2017 qual B E-Popping Balls

  • A+B 个球排成一行,其中左边 A 个是红色的,右边 B 个是蓝色的,你可以选定一对整数 (s,t)s<t),然后重复进行以下操作直到球被取完:将第一个球或第 s 个球或第 t 个球(当前球的数量必须够才能执行)拿出来。求最后拿出来的球的颜色序列有多少种不同方案数。答案对 109+7 取模。方案不同当且仅当存在某一次取出的球的颜色不同。A,B2000

注意到这个 s 其实很尴尬。当 s 位于红球时我们可以选择 1,位于蓝球时选择 t,都可以保证当前选择的颜色不变,所以 s 只可能在球总数 <t 后选择。

所以现在我们只考虑选择 t。和 s 上述的结论一样,那么 t 只可能选择蓝球。那么 t 选择一行中第一个蓝球一定不劣,且这样不会算重,因为最终序列中蓝球第一次出现的位置不同方案必定不同,且其位置显然在区间 [1,A+1] 中所以也不会漏。此时我们钦定 t 为取走第一个蓝球的位置。

所以我们最终序列一定是由 A+1t 个红球开始然后接着一个蓝球。我们枚举 t[1,A+1t] 后剩下了 t1 和红球和 B1 个蓝球。此时我们还需要取走 B1 个球才能轮到 s 的回合。

注意此时剩下的红蓝球没有限制,所以假设最后 B1 个球中有 i 个蓝球,那么我们就需要取走 i 个红球和 B1i 个蓝球,方案数为 (B1i)

现在终于来到 s 的回合,剩下 t1i 个红球和 i 个蓝球,做法和 t 的回合一模一样,也是形如钦定 s 为剩下第一个蓝球然后巴拉巴拉之类的。

此时我们的复杂度是 O(n4)s,t 的回合各需要枚举 n2 级别),但是将式子写出来是形如

ti(B1i)sj(i1j)

那么后面两个 就可以二次前缀和优化了,复杂度 O(n2)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=2e3+4;
const int mod=1e9+7;

int a,b,C[N][N],f[N][N],g[N][N],ans;

void init(int n){
	for(int i=0;i<=n;i++){
		C[i][0]=g[i][0]=f[i][0]=1;
		for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
		for(int j=1;j<=n;j++)f[i][j]=(f[i][j-1]+C[i][j])%mod,g[i][j]=(g[i][j-1]+f[i][j])%mod;
	}
	return;
}

main(){
	scanf("%lld%lld",&a,&b),init(N-2);
	for(int t=1;t<=a+1;t++){
		for(int i=0;i<t;i++){
			if(!i)ans++;
			else ans=(ans+g[i-1][t-i-1]*C[b-1][i]%mod)%mod;
		}
	}
	printf("%lld",ans);
}
  • Gym104337-D Darkness II

  • 网格平面上有 n 个格子是黑色的。每秒钟,若一个白色格子和两个及以上的黑色格子四相邻,它会变成黑色,求当不再有格子变色时,黑色格子的个数。n105

显然最终局面下一定是形成若干个不交的黑色矩形。合并的过程就是两个黑色矩形合并。我们考虑两个黑色矩形什么情况下会合并。不难发现若存在点 (x,y) 使得其距离某个黑色矩形的曼哈顿距离 2,那么黑色矩形就是并上 (x,y),这里的合并就是 xl,xr,yl,yrminmax,也就是合并为新的大黑色矩形。

所以初始的黑色点看作大小为 1×1 的矩形即可,然后查询随便一个和它有交的矩形然后合并。问题转化为二维平面上若干矩形,求两两合并后最后的所有矩形。

有个小可爱不会这个经典的 trick,怎么回事呢?

我们按照 xr 排序,对于 y 轴离散化后建立线段树,每次查询一段区间 [l,r] 内满足矩形右端点大于左端点的矩形编号。

注意我们按照 xr 排序了,所以加入的矩形可以看作一个栈,每次拿出栈顶比较是否 xl 即可。然后考虑怎么处理与 [l,r] 有交即可这个限制呢?(因为只是要求两个矩形之间有交而不是包含)

建立两棵线段树,我们每次修改时插入区间 [yl,yr],在第一棵线段树上所以遍历到的区间都把区间编号 push 进去,当访问到某一个区间满足 yllryr 时在第二棵线段树中把区间编号 push 进去(此时第一棵不 push)。

对于查询时,我们反过来,访问到一个节点就查询第二棵,遇到完全被包含的区间就查询第一棵即可。时间复杂度是优秀的 O(nlogn)。很好写。

#include<bits/stdc++.h>
using namespace std;

#define ls p<<1
#define rs p<<1|1
#define int long long

const int N=1e6+7;

int n,m,lsh[N],ans;
pair<int,int>a[N];
vector<array<int,4>>rect;
vector<int>f[N],g[N],vis;

void update(int p,int l,int r,int s,int t,int i){
	if(t<l||s>r)return;
	f[p].push_back(i);
	if(s<=l&&r<=t){
		g[p].push_back(i);
		return;
	}
	int mid=(l+r)>>1;
	update(ls,l,mid,s,t,i),update(rs,mid+1,r,s,t,i);
	return;
}

int query(int p,int l,int r,int s,int t,int xl){
	if(t<l||s>r)return -1;
	while(!f[p].empty()&&vis[f[p].back()])f[p].pop_back();
	while(!g[p].empty()&&vis[g[p].back()])g[p].pop_back();
	if(!g[p].empty()&&rect[g[p].back()][1]>=xl)return g[p].back();
	if(s<=l&&r<=t){
		if(!f[p].empty()&&rect[f[p].back()][1]>=xl)return f[p].back();
		return -1;
	}
	int mid=(l+r)>>1,res=query(ls,l,mid,s,t,xl);
	if(~res)return res;return query(rs,mid+1,r,s,t,xl);
}

signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		int x,y;scanf("%lld%lld",&x,&y);
		a[i]=make_pair(x,y);
		lsh[++m]=y-1,lsh[++m]=y,lsh[++m]=y+1,lsh[++m]=y+2;
	}sort(a+1,a+n+1),sort(lsh+1,lsh+m+1),m=unique(lsh+1,lsh+m+1)-lsh-1;
	for(int i=1;i<=n;i++){
		int xl=a[i].first,xr=xl,yl=a[i].second,yr=yl;
		yl=lower_bound(lsh+1,lsh+m+1,yl)-lsh,yr=lower_bound(lsh+1,lsh+m+1,yr)-lsh;
		while(1){
			int t=query(1,1,m,yl-2,yr+2,xl);
			if(t==-1)t=query(1,1,m,yl,yr,xl-2);
			if(t==-1)t=query(1,1,m,yl-1,yr+1,xl-1);
			if(t==-1)break;
			xl=min(xl,rect[t][0]),yl=min(yl,rect[t][2]),yr=max(yr,rect[t][3]);
			vis[t]=1;
		}
		update(1,1,m,yl,yr,rect.size()),rect.push_back({xl,xr,yl,yr}),vis.push_back(0);
	}
	for(int i=0;i<rect.size();i++)if(!vis[i])ans+=(rect[i][1]-rect[i][0]+1)*(lsh[rect[i][3]]-lsh[rect[i][2]]+1);
	printf("%lld\n",ans);
    return 0;
}
  • [AGC039F] Min Product Sum

  • 有一个大小为 n×m 的矩阵。矩阵中每个数的取值都是 [1,K]。于一个矩阵,定义函数 f(x,y) 为:第 x 行和第 y 列的一共 n+m1 个数中的最小值。对于一个矩阵,定义其权值为 nx=1my=1f(x,y)。求出所有 Knm 种矩阵,每个矩阵的权值和。1n,m,k100108D109

将权值刻画为组合意义上,就是一个矩阵 B 满足 Bi,jmin(minkAi,k,minkAk,j),进一步转化就是 Bi 行的 maxAi 行的 min,列同理。权值和就是合法 (A,B) 数量。

对于这种权值最大/最小值的限制,我们容易想到将数值将小到大插入。假设当前插入数值为 k 个点。设 dp 状态:fk,i,j 表示当前填入的所有点数值都 k 且有 iB 行被钦定最大值,jA 列被钦定最小值。

我们考虑 B 中新增一个行的最大值 k 和新增 A 中一个列的最小值 k,转移分为两步:

  1. 新增 B 的一行,考虑这个行上的每一个位置,如果对应的列上 A 的最小值已经确定(且一定 <k),那么为了适配限制,我们往这个位置的 A 上填入一个 k 的数;如果对应的列上 A 的最小值还未确定,那么可以在 B 中填入一个 k 的数,并且要求这些数中至少有一个 =k。方案数就是 (Kk+1)j(kmj(k1)mj)

  2. 新增 A 的一列。与上述同理。

实现时 dp 状态增加两维来表示当前在转移的第 1/2 步,注意将 dp 的转移系数预处理。时间复杂度 O(n4)

#include<bits/stdc++.h>
using namespace std;

const int N=120;

int n,m,K,mod,C[N][N],coef[N][N][N][2],dp[N][N][N][2];

int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=1ll*res*a%mod;
		a=1ll*a*a%mod;
		b>>=1;
	}
	return res;
}

void init(int n){
	C[0][0]=1;
	for(int i=1;i<=n;i++){
		C[i][0]=1;
		for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
	}
	return;
}

int main(){
	scanf("%d%d%d%d",&n,&m,&K,&mod),init(N-2);
	for(int k=1;k<=K;k++)for(int i=0;i<=m;i++){
		int t=1ll*qpow(K-k+1,i)*((qpow(k,m-i)-qpow(k-1,m-i)+mod)%mod)%mod;
		coef[k][i][0][0]=1;
		for(int j=1;j<=n;j++)coef[k][i][j][0]=1ll*coef[k][i][j-1][0]*t%mod;
	}
	for(int k=1;k<=K;k++)for(int i=0;i<=n;i++){
		int t=1ll*qpow(k,n-i)*((qpow(K-k+1,i)-qpow(K-k,i)+mod)%mod)%mod;
		coef[k][i][0][1]=1;
		for(int j=1;j<=m;j++)coef[k][i][j][1]=1ll*coef[k][i][j-1][1]*t%mod;
	}
	dp[1][0][0][0]=1;
	for(int k=1;k<=K;k++)for(int i=0;i<=n;i++)for(int j=0;j<=m;j++){
		for(int t=0;t<=n-i;t++)
			dp[k][i+t][j][1]=(dp[k][i+t][j][1]+1ll*dp[k][i][j][0]*C[n-i][t]%mod*coef[k][j][t][0]%mod)%mod;
		for(int t=0;t<=m-j;t++)
			dp[k+1][i][j+t][0]=(dp[k+1][i][j+t][0]+1ll*dp[k][i][j][1]*C[m-j][t]%mod*coef[k][i][t][1]%mod)%mod;
	}
	printf("%d",dp[K+1][n][m][0]);
	return 0;
}
  • CF653G Move by Prime

  • 一个长度为 n 的数列 ai,你可以进行任意次操作:将其中一个数乘上或者除以一个质数。使得最终所有数相同,并使得操作数尽可能小。求所有子序列的操作数之和是多少。1n,ai3×105

将每个质数独立计算贡献,对于一个子序列中质数指数的序列 x,设我们最终全部被调整为 mid,代价就是 |ximid|,显然 mid 应该取 x 的中位数。不难发现 mid 的贡献最终会被消去,来考虑每个 xi 产生的贡献。

假设排名为 ixi 左边选择了 a 个在子序列中,右边选择了 b 个,那么不难发现贡献为:当 a<bxia>bxi,否则为 0

发现我们只关系 ab 的大小而不是 a,b 各自的值。但是直接枚举 ab 会因为没有组合意义而算重。一种常见二点套路是设 j=i1a+b,表示左边没有选择进子序列的个数减去右边加入子序列的个数,这样就出现组合意义。我们得到 xi 的贡献:

xi(n1j=i(n1j)i2j=1(n1j))

所以预处理 (n1j) 的前缀和,然后再处理出 n1j=i(n1j)i2j=1(n1j) 的前缀和,可以做到 O(n) 计算。

对于枚举质数以及 xi 的部分使用类似与埃式筛暴力枚举即可,复杂度 O(VloglogV)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=3e5+6;
const int mod=1e9+7;

int n,fac[N],ifac[N],c[N],f[N],ans,rk[N],cnt[N],vis[N],a[N];

int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

int C(int n,int m){
	if(n<0||m<0||n<m)return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

void init(int n){
	fac[0]=ifac[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
	ifac[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
	c[0]=C(n-1,0);for(int i=1;i<n;i++)c[i]=(c[i-1]+C(n-1,i))%mod;
	for(int i=1;i<=n;i++)f[i]=(f[i-1]+c[n-1]-c[i-1]-c[i-2]+mod+mod)%mod;
	return;
}

signed main(){
	scanf("%lld",&n),init(n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),cnt[a[i]]++;
	for(int i=2;i<N;i++)if(!vis[i]){
		for(int j=i;j<N;j+=i)vis[j]=1;
		int k=1;
		for(int j=i;j<N;j*=i,k++){
			rk[k]=0;
			for(int t=j;t<N;t+=j)rk[k]+=cnt[t];
			if(!rk[k])break;
		}rk[k]=0;
		for(int j=k-1;j;j--)ans=(ans+j*(f[rk[j]]-f[rk[j+1]]+mod)%mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}
  • CF1656F Parametric MST

  • 给定一张 n 个点的无向完全图 Kn(t) ,点 i 和点 j 之间的边的边权 wi,j(t)aiaj+t(ai+aj) ,其中 t 为任意实数。定义 f(t)Kn(t) 的最小生成树的边权和。判断 f(t) 是否有最大值并输出。n2×105,ai106

我感觉这个 F 2600 比下面同一场的 H 3200 难,怎么回事呢?

a 排序,考虑无解的情况,也就是 t 的系数恒正/负,所以分析每个点的连向。

因为是完全图,一个显然正确的贪心策略是 [1,i] 的点连向 an[i+1,n] 的点连向 ai。那么总代价就是

W=ij=1ajan+nj=i+1aja1+t(ni=1ai+(ni)a1+ian)

注意到 wi,j(t)=(ai+t)(aj+t)t2,这在 t 固定时形式很好。

所以无解时 i,ai+t>0 或者 <0,所以带入 t 的系数得到无解的两种情况:ni=2ai+(n1)ai>0 或者 ni=2ai+(n1)an<0

容易得到 t[an,a1],上述 wi,j(t) 的变形形式很好启发我们枚举 t,那么引发一个关键的 observation:t 的值一定为某个 ai。这也是自然的,观察 W 的式子,当 i 确定时这是一个关于 t 的一次函数,而当 t[ai+1,ai]i 不变所以 t 必然取到端点处才有最值。

所以枚举 t 然后带入 W 式子即可计算出答案。记得减去重边 (a1an)。

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=2e5+6;

int T,n,a[N],s[N];

void solve(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
	if(s[n]-s[1]+a[1]*(n-1)>0||s[n-1]+a[n]*(n-1)<0){puts("INF");return;}
	int ans=-1e18;
	for(int i=1;i<=n;i++){
		int res=a[n]*s[i]+a[1]*(s[n]-s[i])+(-a[i])*(s[n]+i*a[n]+(n-i)*a[1])-a[1]*a[n]-(-a[i])*(a[1]+a[n]);
		ans=max(ans,res);
	}
	printf("%lld\n",ans);
	return;
}

signed main(){
	scanf("%lld",&T);
	while(T--)solve();
	return 0;
}
  • CF1656H Equal LCM Subsets

  • 两个集合A,B,大小分别为n,m,你需要找两个非空子集 SAA,SBB,使得 SA 中的元素的 lcmSB 中的元素的 lcm 相等。判断是否有解并输出任意一种。1n,m103,1ai,bi4×1036

将寻找子集转化为在原来的集合中删去一些数后满足条件。考虑怎么样的一个 ai 必定会被删去:当存在某个质数 p 使得 vp(ai)>maxjvp(bj),其中 vp(x) 表示 xp 的幂数。

由于 ai,bi 太大无法分解质因数,考虑将形式转化,也就是 :gcdmj=1aigcd(ai,bj),那么这个就容易判断了。

暴力维护就是维护当前集合剩下的数然后每次暴力判断当前这个数是否需要删除,正确性显然。优化也是简单的,由于 n,m 实在太小所以对于每个 ai,bj 一共开 n+m 棵线段树来维护 gcdmj=1aigcd(ai,bj) 这个式子,而每次删除一个数就相当于在另一边单点修改为 0,因为 gcd(0,x)=x 不影响。然后每次询问全局答案。时间复杂度 O((n+m)2log(n+m)logV)

#include<bits/stdc++.h>
using namespace std;

#define ls p<<1
#define rs p<<1|1
#define Int __int128

const int N=2e3+4;

int n,m,T,vis[N][2],A,B;
Int a[N],b[N];

Int gcd(Int x,Int y){return y?gcd(y,x%y):x;}

Int read(){
	Int x=0;char ch=getchar();
	while(!isdigit(ch))ch=getchar();
	while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
	return x;
}

void write(Int x){
	int t[37],_=0;
	while(x)t[++_]=x%10,x/=10;
	while(_)putchar(t[_]+'0'),_--;
	putchar(' ');return;
}

struct SegmentTree{
	Int tr[N<<2];
	void pushup(int p){tr[p]=gcd(tr[ls],tr[rs]);}
	void build(int p,int l,int r){
		tr[p]=0;if(l==r)return;
		int mid=(l+r)>>1;
		build(ls,l,mid),build(rs,mid+1,r);
		return;
	}
	void update(int p,int l,int r,int ps,Int val){
		if(l==r){tr[p]=val;return;}
		int mid=(l+r)>>1;
		if(ps<=mid)update(ls,l,mid,ps,val);
		else update(rs,mid+1,r,ps,val);
		pushup(p);
		return;
	}
}t[N][2];

void solve(){
	scanf("%d%d",&n,&m),A=n,B=m;
	for(int i=1;i<=n;i++)a[i]=read(),t[i][0].build(1,1,m),vis[i][0]=0;
	for(int i=1;i<=m;i++)b[i]=read(),t[i][1].build(1,1,n),vis[i][1]=0;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)t[i][0].update(1,1,m,j,a[i]/gcd(a[i],b[j]));
	for(int i=1;i<=m;i++)for(int j=1;j<=n;j++)t[i][1].update(1,1,n,j,b[i]/gcd(b[i],a[j]));
	while(A>0||B>0){
		bool fl=0;
		for(int i=1;i<=n;i++)if(!vis[i][0]&&t[i][0].tr[1]>1){
			for(int j=1;j<=m;j++)t[j][1].update(1,1,n,i,0);
			A--,vis[i][0]=1,fl=1;
		}
		for(int i=1;i<=m;i++)if(!vis[i][1]&&t[i][1].tr[1]>1){
			for(int j=1;j<=n;j++)t[j][0].update(1,1,m,i,0);
			B--,vis[i][1]=1,fl=1;
		}
		if(!fl)break;
	}
	if(!A||!B){puts("NO");return;}
	puts("YES"),printf("%d %d\n",A,B);
	for(int i=1;i<=n;i++)if(!vis[i][0])write(a[i]);puts("");
	for(int i=1;i<=m;i++)if(!vis[i][1])write(b[i]);puts("");
	return;
}

int main(){
	scanf("%d",&T);
	while(T--)solve();
	return 0;
}
  • UOJ351. 新年的叶子

  • 对于一棵树,每次随机染黑一个叶子(可能会重复染黑),期望多少次后直径变小。n5×105

先把直径拎出来,我们考虑划分可能的直径的端点。若直径长度为奇数,那么直径中心为边,那么可能的直径端点被划分成两个集合,否则会被划分为若干个集合。

而直径变小也就是不断染黑知道仅剩一个集合没有被全染黑。先计算出若剩下 i 个叶子没有被染黑,那么一次有效染色的概率就是 Mi,其中 M 是叶子总数。

设所有集合原大小为 m,考虑最后剩下的那个集合,设其集合大小原本为 szi,结束时剩下 j 个点还是白色,可以得出贡献:

1m!Ajszi[(mj)!(mj1)!(szij)]mk=j+1pk

含义为:Ajsziszi 中选择 j 个白色的排列方案,(mj)!(mj1)!(szij) 则是所有染色排列减去最后一次染色染到当前集合的方案,否则会算重。至于 mk=j+1pk 通过前缀和预处理即可。时间复杂度 O(n)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=5e5+6;
const int mod=998244353;

int n,m,ans,rt,len,cnt,sz[N],p[N],fac[N],inv[N],fa[N];
vector<int>g[N];

void init(int n){
	fac[0]=inv[0]=inv[1]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
	for(int i=2;i<=n;i++)inv[i]=inv[mod%i]*(mod-mod/i)%mod;
	for(int i=1;i<=n;i++)p[i]=(p[i-1]+n*inv[i]%mod)%mod,inv[i]=inv[i-1]*inv[i]%mod;
	return;
}

void dfs(int u,int faa,int d){
	if(d>len)len=d,rt=u;
	for(int v:g[u])if(v!=faa)fa[v]=u,dfs(v,u,d+1);
	return;
}

void Dfs(int u,int faa,int d,int i){
	if(d==len/2)sz[i]++,m++;
	for(int v:g[u])if(v!=faa)Dfs(v,u,d+1,i);
	return;
}

signed main(){
	scanf("%lld",&n);
	for(int i=1;i<n;i++){
		int u,v;scanf("%lld%lld",&u,&v);
		g[u].push_back(v),g[v].push_back(u);
	}
	for(int i=1;i<=n;i++)m+=(g[i].size()==1);
	init(m),m=0,dfs(1,0,0),len=0,dfs(rt,0,0);
	for(int i=1;i<=len/2;i++)rt=fa[rt];
	if(len&1)Dfs(rt,fa[rt],0,++cnt),Dfs(fa[rt],rt,0,++cnt);
	else for(int v:g[rt])Dfs(v,rt,1,++cnt);
 	for(int i=1;i<=cnt;i++)for(int j=1;j<=sz[i];j++){
		int res=(fac[m-j]-fac[m-j-1]*(sz[i]-j)%mod+mod)%mod;
		res=res*(p[m]-p[j]+mod)%mod,res=res*fac[sz[i]]%mod*inv[sz[i]-j]%mod;
		ans=(ans+inv[m]*res%mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}
  • CF627D Preorder Test

  • 给出一棵无根树,每个节点有一个权值。你可以指定树根和访问顺序,使得 dfs 序中前 k 个结点的权值最小值最大,求出这个值。n2×105

先二分答案 k,先随便选择一个点作为根,设 fu 表示 u 及子树内指定 dfs 序后满足 aik 的最长长度。显然对于 fv=szv 的儿子我们可以直接选择,否则我们只能选择剩下的最大值。

统计答案则是 fu+se,表示再选择剩下子树中次大值,因为可以以这个次大值中起始点作为根然后一直访问到 mx 中的那个点即可。子树外的情况不用管因为会被 u 的祖先统计到。

但是这样会 Wrong Answer On Test 25,因为可能子树外全部都是 aik 的点,这导致我们计算 fu 时应该加上 nszu。解决方案也很简单,直接找出 a 最小的点设为根即可就能保证子树外的点必然不是全都 k。时间复杂度 O(nlogn)

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+6;

int n,tot,head[N],f[N],sz[N],a[N],mx,mn,Ans,mid,ans,k,rt;

struct edge{int to,nxt;}e[N<<1];

void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

void dfs(int u,int fa){
	f[u]=1,sz[u]=1;
	int mx=0,se=0;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u),sz[u]+=sz[v];
		if(f[v]==sz[v])f[u]+=f[v];
		else{
			if(f[v]>mx)se=mx,mx=f[v];
			else if(f[v]>se)se=f[v];
		}
	}
	f[u]=a[u]<mid?0:f[u]+mx;
	ans=max(ans,f[u]+se);
	return;
}

int main(){
	scanf("%d%d",&n,&k),mn=1e9;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]),mx=max(mx,a[i]),mn=min(mn,a[i]);
		if(mn==a[i])rt=i;
	}
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(u,v),addedge(v,u);
	}
	int l=mn,r=mx;
	while(l<=r){
		mid=(l+r)>>1,ans=0;
		dfs(rt,0);
		if(ans>=k)Ans=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",Ans);
	return 0;
}
  • CF1693F I Might Be Wrong

  • 给定一个长度为 n01 字符串 S,你可以进行下列操作任意次:选择 S 的一个连续子串 S[l,r],设 cnt0,cnt1 分别表示该子段中字符 0 和字符 1 的数量,将花费 |cnt0cnt1|+1 的代价对 S[l,r] 进行升序排序。求出使 S 本身升序排序所需的最少代价。 n2×105

答案上界显然是 |c0c1|+1,也就是直接操作 [1,n]。那么本质上我们是希望通过多次操作来减少这个代价。

对于每个不同的区间,其代价都是不同的,这是令人烦恼的,我们尝试挖掘一些性质从而刻画 |c0c1|+1 这个代价。考虑这样一个事情:你本可以直接操作 [1,n],但是你会操作多次从而使总代价减少,这启发我们同样对于任意一个区间 [l,r] 进入如下的分析:设最终对某个区间 [l,r] 进行了代价为 d+1 的操作,我们能不能通过操作多次来减少,或者更好地刻画这个代价?

(开始前先钦定所有 0 的个数大于 1 的,否则可以反转整个区间然后每个位置取反。)

事实上是可行的,并且也不难证明:对于任意一个代价为 d+1 的区间可以被分割为 d+1 个代价为 1 的区间。首先初始时有 |c0c1|=d,那么考虑区间开头的数必然是一个 1(我们的区间不应包括开头的 0),找到一个 1 和 0 数量相等的前缀将其排序,这里便花费了 |cc|+1=1 的代价,而此时区间开头又变为 0,所以删去开头的若干个 0,我们就递归到了一个总代价为 d+1c 的区间,那么这一定是不劣于原来的决策的(最劣也就是每次 c=1,那么需要 d+1 次操作)。所以我们的决策是确定的,每次选取一个 c0=c1 的区间进行操作。

后面的就很自然了。我们的目的是将 1 放到数列末尾,那么我们每次肯定是操作一个前缀,否则会有 1 向右移动后又向左移动的操作的产生,这肯定是劣的。而每次选择一个前缀可以保证每个 1 只会向右移动,必定最优。

整理我们的策略:每次找到一个最长的 c0=c1 的前缀,然后删去这 c0 个位于开头的 0。那么这可以通过维护出一个数组 posk 表示最大的位置 i 使得 [1,i]c0c1=k。那么模拟起来就是先跑出初始时再开头的 k 个 0,然后每次 kk+(poskk)/2,意义就是选择 [k+1,posk] 这个区间,然后 k 会跳到 k+(poskk)/2。时间复杂度 O(n)

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+6;

int T,n,cnt,pos[N],now;
char s[N];

void solve(){
	scanf("%d%s",&n,s+1),now=0,cnt=0;
	for(int i=2;i<=n;i++)now+=(s[i]<s[i-1]);
	if(!now){puts("0");return;}pos[0]=0;
	for(int i=1;i<=n;i++)cnt+=s[i]=='0',pos[i]=0;
	if(cnt<n-cnt){
		cnt=n-cnt;
		for(int i=1;i<=n/2;i++)swap(s[i],s[n-i+1]);
		for(int i=1;i<=n;i++)s[i]='0'+'1'-s[i];
	}now=0;
	for(int i=1;i<=n;i++){
		now+=s[i]-'0'?-1:1;
		if(now>=0)pos[now]=i;
	}
	int delta=cnt-(n-cnt),k=1,ans=1;
	while(k<=n&&s[k]=='0')k++;k--;
	while(k<delta)ans++,k+=(pos[k]-k)/2;
	printf("%d\n",ans);
}

int main(){
	scanf("%d",&T);
	while(T--)solve();
	return 0;
}
  • [ABC134F] Permutation Oddness

  • 定义一个排列 p 的权值为 |ipi|,求权值为 k 的长度为 n 的排列 p 数量。n50

本题核心在于刻画 |ipi|。绝对值可以想到分讨后拆开,也可以理解为数轴上两个点之间的距离。虽然这两种都不太能刻画,但是 ipi 可以看作 ipi 连边,那么这会形成很多独立的环,这不利于刻画权值。考虑将 ipi 看作两个不同的事物,比如一个表示小球一个表示盒子,而小球在左边拍成一列,盒子在右边排成一列,这就是一个二分图。那么 |ipi| 就容易想到:ipi 连边后与水平线的交点个数。水平线定义为两个相邻小球之间连向对应的两个相邻盒子之间。

那么这是一种比较自然的刻画方式。得出这个模型后 dp 不难。设 fi,j,k 表示只考虑 1i 的小球与盒子,有 j 个小球和盒子没有被选择,当前权值和为 k,即所有已连线与水平线的交点。转移分类当前这一层进行几次配对 (0/1/2)即可,是 trival 的,不做赘述。时间复杂度 O(n4)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=52;
const int M=N*N;
const int mod=1e9+7;

int n,K,dp[N][N][M];

signed main(){
	scanf("%lld%lld",&n,&K),dp[0][0][0]=1;
	for(int i=1;i<=n;i++)for(int j=0;j<=i;j++)for(int k=2*j;k<=K;k+=2){
		dp[i][j][k]=dp[i-1][j][k-2*j]*(2*j+1)%mod;
		dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-2*j]*(j+1)%mod*(j+1)%mod)%mod;
		if(j)dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k-2*j])%mod;
	}
	printf("%lld\n",dp[n][0][K]);
	return 0;
}
  • [AGC047F] Rooks

  • n 个车在一张无限大的棋盘上,第 i 个在 (xi,yi)。每行每列最多一个车。有一个卒,会替换第 i 个车,可以走八连通,但是不能走到被车攻击的地方。吃车的时候可以走对角,否则只能走上下左右。对于 i=1n,求出吃掉最多的车时的最小步数。n106

先将 xi 排序后将 x,y 离散化。那么注意到这个卒吃掉的车的 x,y 必定是一个完整的区间。所以容易想到区间 dp,fl,r,0/1 表示当前吃掉了 [l,r] 内的车并且在 l/r 上。对于转移则算出 mn=mini[l,r]yi,mx=maxi[l,r]yi,那么弱向左扩展一格便判断 yl1 是否为 mn1 或者 mx+1,向右同理。这样是 O(n3) 的,因为要枚举起点 i 来初始化 fi,i,0/1=0。那么这样会有很多重复状态,因为对于一个固定的区间 [l,r] 一定存在一个极大的可以到达的区间 [L,R],不管按什么顺序操作最后都能到这个极大的区间。所以考虑状态定义反过来为除了 [l,r] 都吃掉了的答案。那这就是 O(n2) 的了。

注意到如果区间内满足 |yiyi+1|=1,也就是区间为一段 x,y 的连续段,那么此时我们删除的策略是固定的:删除 l 后就一定会删除 l+1,r。那么就可以将这个区间看作一个点,然后每次扩展到它的下一个极大区间 [L,R]。对于这个做法状态数只有 O(n) ,直接记忆化搜索即可。

如何证明?首先先将整体看作若干段 |yi=yi+1|=1 的连续段,呈现在平面上也就是若干个正方形相互包含,那么我们的状态数也就输层数级别的,即相互包含的数量。而对于两个区间的合并只会有一段连续段被两个正方形所共同囊括,所以每个正方形最多只会被包含两次,所以状态数 O(n)。使用 map 记忆化搜索 O(nlogn)

#include<bits/stdc++.h>
using namespace std;

#define ll long long

const int N=2e5+6;
const ll inf=1e18;

int n,L[N],R[N];ll ans[N];
vector<int>lsh;
map<int,ll>dp[2][N];

struct node{
	int x,y,i;
	bool operator<(const node&rhs)const{return x<rhs.x;}
}a[N];

int dis(node a,node b){return abs(a.x-b.x)+abs(lsh[a.y]-lsh[b.y]);}

ll dfs(int l,int r,int mn,int mx,int pos){
	if(dp[pos][l][r])return dp[pos][l][r];
	else dp[pos][l][r]=inf;
	node now=pos?a[r]:a[l];
	if(l>1&&(a[l-1].y==mn-1||a[l-1].y==mx+1)){
		int k=L[l-1];
		dp[pos][l][r]=min(dp[pos][l][r],dfs(k,r,min(mn,a[k].y),max(mx,a[k].y),0)+dis(now,a[k]));
	}
	if(r<n&&(a[r+1].y==mn-1||a[r+1].y==mx+1)){
		int k=R[r+1];
		dp[pos][l][r]=min(dp[pos][l][r],dfs(l,k,min(mn,a[k].y),max(mx,a[k].y),1)+dis(now,a[k]));
	}
	if(dp[pos][l][r]==inf)dp[pos][l][r]=-(r-l);
	return dp[pos][l][r];
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y),a[i].i=i,lsh.push_back(a[i].y);
	sort(lsh.begin(),lsh.end());
	for(int i=1;i<=n;i++)a[i].y=lower_bound(lsh.begin(),lsh.end(),a[i].y)-lsh.begin();
	sort(a+1,a+n+1);
	L[1]=1,R[n]=n;
	for(int i=2;i<=n;i++)L[i]=abs(a[i].y-a[i-1].y)==1?L[i-1]:i;
	for(int i=n-1;i;i--)R[i]=abs(a[i].y-a[i+1].y)==1?R[i+1]:i;
	for(int i=1;i<=n;i++)ans[a[i].i]=dfs(i,i,a[i].y,a[i].y,0);
	for(int i=1;i<=n;i++)printf("%lld\n",ans[i]);
	return 0;
}
  • [ARC146E] Simple Speed

  • 给定长为 n 的序列 ai ,求满足要求的序列 b 的数量:元素为 [1,n]i 刚好出现 ai 次,且 |bibi1|=1n,ai2×105

有个笨蛋把题目看成 |bibi1|1 了。

按照值域加入数,从小往大放,考虑加入完所有 i 后加入 i+1。首先 i+1 必须囊括所有相邻 i 之间的空位否则序列非法。而 i+1 也只能放在相邻 i 的中或者边界上的 i 的旁边。呼之欲出一个简单的 dp:fi,j,0/1,0/1 表示当前加入到了 i,有 j 对相邻的 i,序列最左边/最右边是不是 i

瓶颈在于 j 的状态,而显然 j 每次的变化量范围很小。如果不考虑两边放置 i 的情况,那么每次的 j 都是确定的。所以两边放/不妨只会造成 1 的影响,j 的变化量大约 5,时间复杂度 O(n)

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=2e5+6;
const int mod=998244353;

int n,a[N],num[N],ans,dp[N][5][2][2],fac[N<<1],ifac[N<<1];

int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

void init(int n){
	fac[0]=ifac[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
	ifac[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
	return;
}

int C(int n,int m){
	if(n<0||m<0||n<m)return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

inline void add(int&x,int y){x=(x+y>=mod?x+y-mod:x+y);}

signed main(){
	scanf("%lld",&n),init(N*2-2);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	num[1]=a[1]-1,dp[1][2][1][1]=1;
	for(int i=2;i<=n;i++){
		num[i]=a[i]-num[i-1];
		for(int j=0;j<5;j++)for(int l=0;l<2;l++)for(int r=0;r<2;r++)if(dp[i-1][j][l][r]){
			int c=num[i-1]+j-2;
			for(int L=0;L<=l;L++)for(int R=0;R<=r;R++){
				if(L&&R)add(dp[i][2-j][L][R],dp[i-1][j][l][r]*C(a[i]-1,c+1)%mod);
				else if(L||R)add(dp[i][3-j][L][R],dp[i-1][j][l][r]*C(a[i]-1,c)%mod);
				else add(dp[i][4-j][L][R],dp[i-1][j][l][r]*C(a[i]-1,c-1)%mod);
			}
		}
	}
	for(int j=0;j<5;j++)if(num[n]+j-2==0)for(int l=0;l<2;l++)for(int r=0;r<2;r++)add(ans,dp[n][j][l][r]);
	printf("%lld",ans);
	return 0;
}
  • P4845 LJJ 爱数树

  • 给定一棵树,点有点权。要求放置至多 k 个摄像头,每个摄像头可以检测与其距离 r 的所有点,使得所有被检测到的点的权值和最大。n,k,r103

截止至 2023/12/14 是最优解 439ms /cengyiceng。

每个点只有有摄像头、被摄像头检测到、不被检测到三种情况。

一个显然的树形 dp 是 fu,i,j,k 表示以 u 为根的子树内放了 i 个摄像头,j 表示距离 u 最远的等待点u 的距离,k 表示距离 u 最近的摄像头与其距离。

之所以定义 j 是因为我们钦定每个点只有有摄像头、被摄像头检测到、不被检测到三种情况。而从子树去 dp 时注意有的点会被钦定为被摄像头检测到,然而此时子树内的摄像头并不能检测到它,也就是检测到它的摄像头在子树外面,我们称这样的点为等待点

那么一个极其常见的套路就是 j,k 可以被合并成一维。我记得我做过完全一样套路的题目,不知道是不是忘记写在这里了。

首先 j+k>r,否则此时子树内的等待点就可以直接被子树内最浅的这个摄像头照到。设该等待点被子树外一点 w 上的摄像头检测,那么 dis(u,w)+jr,可以得到 k>dis(u,w),那么 w 上的摄像头可以检测到子树 u 内所有摄像头能检测到的子树外的点,注意对于子树内我只关心 j,所以此时 u 子树内的等在未来不会用任何作用,k 就可以不用记录。而若不存在等待点就只记录 k 就好了。

所以我们转化为两个 dp:fu,i,j,gu,i,j 表示以 u 为根的子树内放置了 i 个摄像头,f 中的 j 表示最浅的摄像头与 u 的距离,gj 表示最深的等待点与 u 的距离。

对于 dp 转移考虑树形背包的形式,先将 fu,fvgu,gv 分别合并,然后考虑 fu+gvfv+gu 的情况,此时判断二者之间的距离,也就是 j+k+1r 的大小关系即可确定转移到新的 fg。复杂度 O(nkr)

再次优化,发现当 krn 时必定可以覆盖所有点,所以答案就是 ai。证明即考虑此时以某个点为根时的树高。若树高 r 则在根放置摄像头即可,否则选择最深点的 r 级祖先放置摄像头(没有就放在根上),每次放置必定至少检测 r 个点,故证毕。

所以此时 krn,时间复杂度 O(nkr)<O(n2)

#include<bits/stdc++.h>
using namespace std;

const int N=1e3+4;
const int NN=N<<1;
const int inf=1e9;

int T,n,k,r,tot,head[N],f[N][NN],g[N][NN],F[NN],G[NN],ans,a[N],sz[N];

struct edge{int to,nxt;}e[N<<1];

void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

int id(int j,int k){return j*(r+2)+k;}
void Max(int&x,int y){x=x<y?y:x;}

void dfs(int u,int fa){
	sz[u]=1;
	for(int i=0;i<=k;i++)for(int j=0;j<=r+1;j++)f[u][id(i,j)]=g[u][id(i,j)]=-inf;
	f[u][id(0,r+1)]=0,f[u][id(1,0)]=g[u][id(0,0)]=a[u];
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		for(int i=0;i<NN;i++)F[i]=G[i]=-inf;
		for(int j1=min(sz[u],k);j1>=0;j1--)for(int k1=0;k1<=r+1;k1++)if(f[u][id(j1,k1)]>=0||g[u][id(j1,k1)]>=0)
		for(int j2=min(sz[v],k-j1);j2>=0;j2--)for(int k2=0;k2<=r+1;k2++)if(f[v][id(j2,k2)]>=0||g[v][id(j2,k2)]>=0){
			int fu=f[u][id(j1,k1)],gu=g[u][id(j1,k1)],fv=f[v][id(j2,k2)],gv=g[v][id(j2,k2)];
			Max(F[id(j1+j2,min(k1,k2+1))],fu+fv),Max(G[id(j1+j2,max(k1,min(r+1,k2+1)))],gu+gv);
			if(k2+1+k1<=r)Max(F[id(j1+j2,k1)],fu+gv),Max(F[id(j1+j2,min(r+1,k2+1))],fv+gu);
			else Max(G[id(j1+j2,min(r+1,k2+1))],fu+gv),Max(G[id(j1+j2,k1)],fv+gu);
		}
		sz[u]+=sz[v],memcpy(f[u],F,sizeof(f[u])),memcpy(g[u],G,sizeof(g[u]));
	}
	return;
}

void solve(){
	scanf("%d%d%d",&n,&k,&r),tot=ans=0;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),head[i]=0;
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(u,v),addedge(v,u);
	}
	if(k*r>=n){
		for(int i=1;i<=n;i++)ans+=a[i];
		printf("%d\n",ans);
		return;
	}
	dfs(1,0);
	for(int i=0;i<=r+1;i++)ans=max(ans,f[1][id(k,i)]);
	printf("%d\n",ans);
	return;
}

int main(){
	scanf("%d",&T);
	while(T--)solve();
	return 0;
}

本文作者:trsins

本文链接:https://www.cnblogs.com/trsins/p/17970732

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   trsins  阅读(25)  评论(0编辑  收藏  举报
历史上的今天:
2022-01-17 【做题记录】Ynoi2018 天降之物
2022-01-17 【学术】连分数
2022-01-17 【做题记录】Ynoi2015 盼君勿忘
2022-01-17 【做题记录】BJOI2016 水晶
2022-01-17 【做题记录】P4965 薇尔莉特的打字机
2022-01-17 【做题记录】POI2011 Lightning Conductor
2022-01-17 【做题记录】CF961G Partitions
点击右上角即可分享
微信分享提示