动态规划题单4

91.[ARC114F] Permutation Division

Bob 的策略一定是按照开头从大到小排。
所以最后的答案一定不小于原来的排列。

所以我们要使得最后的排列 \(Q\) 和原排列 \(P\) 的 LCP 尽可能长。
也就是说最后分段的形式满足:(\(t_i\) 是每一段的开头)

  1. \(p_1=p_{t_1} > p_{t_2} > ... > p_{t_m}\)
    不然的话,Bob 可以把后面的放到前面去。
    这些段就是 LCP 长度。
  2. \(p_{t_{m+1}},p_{t_{m+2}},...,p_{t_k} < p_{t_m}\)
    此时 Bob 会把 \(p_{t_{m+1}},p_{t_{m+2}},...,p_{t_k}\) 的 max 作为 \(q_{t_{m+1}}\)

考虑二分 LCP 的长度 \(len\),枚举 \(m\),对于每个 \(m\) 我肯定是求出 \(p_{t_m}\) 的最大值,这样后面的段的选择会变多,设 \([len+1,n]\)\(y\) 个数 < \(p_{t_m}\) 那么,如果 \(y+m\ge k\) 就是可行的,而 \(p_{t_{m+1}},p_{t_{m+2}},...,p_{t_k}\) 一定是选择最小的那几个。
于是问题变成求长度为 \(m\) 的下降子序列的末尾的最大值,这可以用 \(O(n\log n)\) 的 DP 来实现。

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=2e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,a[N],b[N],pos[N],g[N],d,cnt[N],t[N];
bool flag[N];
int check(int mid){  //返回最大的 m 
	if(mid==0) return 0;
	for(int i=1;i<=n;i++) g[i]=0;
	g[1]=-a[1],d=1;  //取 - 变成求最长上升子序列 
	for(int i=2;i<=mid;i++){
		int l=upper_bound(g+1,g+d+1,-a[i])-g;
		if(l==1) continue;
		g[l]=-a[i];
		d=max(d,l);
	}
	for(int i=1;i<=n;i++) cnt[i]=0;
	for(int i=mid+1;i<=n;i++) cnt[a[i]]++; 
	for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];  //cnt[i] 几位 [len+1,n] 中小于 i 的个数
	for(int m=d;m>=1;m--){
		if(m+cnt[-g[m]]>=k) return m;
	} 
	return 0;   
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),k=read();
	for(int i=1;i<=n;i++) a[i]=read(),b[i]=a[i],pos[a[i]]=i;
	int l=1,r=n,mid,len=0;
	while(l<=r){
		mid=(l+r)>>1;
		if(check(mid)) len=mid,l=mid+1;
		else r=mid-1;
	} 
	int m=min(k,check(len));
	
	sort(b+len+1,b+n+1); 
	for(int i=len+1,j=1;j<=k-m;i++,j++) t[j]=pos[b[i]],flag[t[j]]=true;
	for(int i=1;i<=len;i++) printf("%d ",a[i]);
	for(int i=k-m;i>=1;i--){  //注意 Bob 对于后面那几段是从大到小放的 
		for(int j=t[i];j<=n&&(j==t[i] || !flag[j]);j++){
			printf("%d ",a[j]);
		}
	}
	puts("");
	return 0;
}

92.[SDOI2011] 消耗战

虚树上 dp 的裸题。

我们称那些有资源的点为关键点

首先这题单次询问有个 \(O(n)\) 的树形 DP。
即设 \(f_u\) 表示 \(u\) 号点与其子树中的所有关键点不连通的最小代价,那么枚举儿子 \(v\)

  1. \(v\) 不是关键点:\(f_u += \min(f_v,w(u,v))\)
  2. \(v\) 是关键点:\(f_u += w(u,v)\)

然后对所有关键点建出虚树在虚树上做这个 DP 就可以了。(应该不会有人看到 \(92\) 题了还不会虚树吧。)

code

#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=2.5e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,T,tot,head[N],to[N<<1],val[N<<1],Next[N<<1];
void add(int u,int v,int w){
	to[++tot]=v,val[tot]=w,Next[tot]=head[u],head[u]=tot;
}
int dfn[N],dep[N],fa[N][25],ming[N][25],num;
void dfs(int u){
	dfn[u]=++num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa[u][0]) continue;
		fa[v][0]=u,ming[v][0]=w;
		dep[v]=dep[u]+1;
		dfs(v);
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}
int ask(int x,int y){
	int Min=LLONG_MAX;
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) Min=min(Min,ming[x][t]),x=fa[x][t];
	if(x==y) return Min;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) Min=min({Min,ming[x][t],ming[y][t]}),x=fa[x][t],y=fa[y][t];
	return min({Min,ming[x][0],ming[y][0]}); 	
}
int k,a[N],t[N],cnt;
bool flag[N];
vector<PII> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=0;
	t[++cnt]=1,t[++cnt]=a[k];
	for(int i=1;i<k;i++){
		t[++cnt]=a[i];
		t[++cnt]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+cnt+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=unique(t+1,t+cnt+1)-t-1;
	for(int i=1;i<=cnt;i++) G[t[i]].clear();
	for(int i=1;i<cnt;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back({t[i+1] , ask(t[i+1],lca)});   //单向边即可 
	}
}
int f[N];
void DP(int u){
	f[u]=0;
	for(PII e:G[u]){
		int v=e.fi,w=e.se;
		DP(v);
		if(flag[v]) f[u]+=w;
		else f[u]+=min(w,f[v]);
	}
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w),add(v,u,w);
	}
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
			ming[u][i]=min(ming[u][i-1] , ming[fa[u][i-1]][i-1]);
		}
	}
	T=read();
	while(T--){
		k=read();
		for(int i=1;i<=k;i++) a[i]=read(),flag[a[i]]=true;
		Build_Virtual_Tree();
		DP(1);
		printf("%lld\n",f[1]);
		for(int i=1;i<=k;i++) flag[a[i]]=false;
	}
	return 0;
}

93.[HEOI2014] 大工程

也是裸题。

建立虚树,然后简单树形 DP 求出:\(Size_i\) 表示 \(i\) 子树内关键点数量,\(f_i\) 表示 \(i\)\(i\) 子树内关键点的距离和,\(g_i\) 表示最小距离,\(h_i\) 表示最大距离。
注意到求最终答案时一条路径在有根树上肯定形如 \(x\to lca\to y\) 所以只要在 \(lca\) 处统计他即可。
所以求最终答案时只需要合并不同子树内的答案,具体的:
在用儿子 \(v\) 更新 \(u\) 的 DP 值之前, \(u\) 存的是 \(v\) 之前所有 \(v\) 的兄弟的信息,那么更新全局答案 \((sum,Min,Max)\):

  • \(f_u\times Size_v + w(u,v)\times Size_u\times Size_v + f_v\times Size_u \to sum\)
  • \(Min= \min(Min, g_u+w(u,v)+g_v)\)
  • \(Max= \max(Max, h_u+w(u,v)+h_v)\)

\(w(u,v)\) 表示 \(u,v\) 在虚树上的边权,在原树中就是 \(dep_v-dep_u\)

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,T,tot,head[N],to[N<<1],val[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}
int dfn[N],dep[N],fa[N][25],num;
void dfs(int u){
	dfn[u]=++num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa[u][0]) continue;
		fa[v][0]=u;
		dep[v]=dep[u]+1;
		dfs(v);
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}
int k,a[N],t[N],cnt;
bool flag[N];
vector<int> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=0;
	t[++cnt]=1,t[++cnt]=a[k];
	for(int i=1;i<k;i++){
		t[++cnt]=a[i];
		t[++cnt]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+cnt+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=unique(t+1,t+cnt+1)-t-1;
	for(int i=1;i<=cnt;i++) G[t[i]].clear();
	for(int i=1;i<cnt;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back(t[i+1]);   
	}
}
int sum,ming,maxn,Size[N],f[N],g[N],h[N];
void DP(int u){
	Size[u]=flag[u];
	f[u]=0;
	if(flag[u]) g[u]=h[u]=0;
	else g[u]=INT_MAX,h[u]=INT_MIN;
	for(int v:G[u]){
		DP(v);
		int w=dep[v]-dep[u];
		sum += f[u]*Size[v] + w*Size[u]*Size[v] + f[v]*Size[u];
		ming = min(ming , g[u]+w+g[v]);
		maxn = max(maxn, h[u]+w+h[v]);
		Size[u]+=Size[v];
		f[u]+=f[v]+Size[v]*w;
		g[u]=min(g[u],g[v]+w);
		h[u]=max(h[u],h[v]+w);
	}
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
		}
	}
	T=read();
	while(T--){
		k=read();
		for(int i=1;i<=k;i++) a[i]=read(),flag[a[i]]=true;
		Build_Virtual_Tree();
		sum=0,ming=INT_MAX,maxn=INT_MIN;
		DP(1);
		printf("%lld %lld %lld\n",sum,ming,maxn);
		for(int i=1;i<=k;i++) flag[a[i]]=false;
	}
	return 0;
}

94.[HNOI2014] 世界树

我也不太确定这个题到底该不该放进来,就当复习虚树吧。

虚树是显然的。
\(ans_i\) 表示关键点 \(i\) 最后的答案,称原树上离点 \(u\) 最近的关键点为 \(u\) 的对应点。
对于虚树上的点,可以换根 DP 求出 \(f_i\) 表示距离 \(i\) 号点最近的关键点的编号,我相信大家都会。
那对于不是虚树上的点怎么办?
会发现一个点不在虚树上只有两种情况:

  1. 原树上,他的这棵子树里一个关键点都没有。
  2. 他在虚树上的两个点在原树的那条链上。

对应到虚树上可以变为:

  1. 一个虚树节点 \(u\),他在原树上有一个儿子 \(v\),但 \(v\) 的子树中没有关键点,此时 \(v\) 子树中的所有点的对应点一定都是 \(f_u\),所以把 \(ans_{f_u}\) 加上 \(Size_v\)
  2. 虚树上的两个节点 \(x,y\) 其中 \(x\)\(y\) 的父亲,它们在原树上可能只是祖先后代关系,它们之间的链上还有一些点。
    假设其中有个点 \(c\),当然 \(c\) 上还可能挂了一些子树,显然这些子树的 \(f\) 值和 \(c\)\(f\) 值是一样。
    我们可以找到这条链上的一个分割点 \(u\),使得链上 \([u,y)\) 这段区间的对应点都是 \(f_y\)\((x,u)\) 这段区间的对应点都是 \(f_x\)

那具体怎么求呢?因为我们是不能去原树上遍历的。
对于 1. 中的情况:我们可以转换一下,求一个虚树点 \(u\) 所有不存在关键点的子树的大小的和,可以容斥用 \(Size_u\)\(-1\) (自己),再减去所有有关键点的子树的大小。
求有关键点的子树大小只需要遍历 \(u\) 虚树上的每个儿子 \(son\),在原树上倍增求出 \(son\) 在原树上的 \(dep_{son}-dep_u-1\) 级祖先 \(v\),那么 \(v\) 就是 \(u\) 在原树上的儿子,再让 \(Size_u\) 减去 \(Size_v\) 即可。
最后 \(Size_u \to ans_{f_u}\),注意 \(Size_u\) 还要还原。

对于 2. 中的情况:还是一样的对每个 \(x\),枚举虚树上的儿子 \(y\),然后还是倍增求出 \(x\) 在原树上含有 \(y\) 的那个儿子 \(v\),分界点 \(u\) 是可以二分/倍增求的。
求出 \(u,v\) 之后就可以更新答案了:\(Size_v-Size_u \to ans_{f_x}\)\(Size_u-Size_y \to ans_{f_y}\)
一定要注意不是用 \(Size_x-Size_u\) 去更新 \(ans_{f_x}\)

复杂度是 \(O(\sum m \times \log n)\) 或者 \(O(\sum m \times \log^2 n)\)(如果你用二分套倍增的话)。

code

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,T,tot,head[N],to[N<<1],val[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}
int dfn[N],dep[N],Size[N],fa[N][25],num;
void dfs(int u){
	dfn[u]=++num;
	Size[u]=1;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa[u][0]) continue;
		fa[v][0]=u;
		dep[v]=dep[u]+1;
		dfs(v);
		Size[u]+=Size[v]; 
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}
int Get(int x,int len){
	for(int t=20;t>=0;t--) if(len>>t&1) x=fa[x][t];
	return x;
} 
int dist(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}

int k,h[N],a[N],t[N],cnt;
bool flag[N];
vector<int> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=0;
	t[++cnt]=1,t[++cnt]=a[k];
	for(int i=1;i<k;i++){
		t[++cnt]=a[i];
		t[++cnt]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+cnt+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=unique(t+1,t+cnt+1)-t-1;
	for(int i=1;i<=cnt;i++) G[t[i]].clear();
	for(int i=1;i<cnt;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back(t[i+1]);   
	}
}

int f[N],ans[N];
void DP1(int u){   //先求子树中的关键点到 u 的距离最短的点 
	f[u]=(flag[u])?u:-1;
	for(int v:G[u]){
		DP1(v);
		if(f[u]==-1) f[u]=f[v];
		else if(dist(f[u],u) > dist(f[v],u)) f[u]=f[v];
		else if(dist(f[u],u) == dist(f[v],u)) f[u]=min(f[u],f[v]);
	}
}
void DP2(int u){ //换根 
	for(int v:G[u]){
		//用 u 取更新儿子,这里直接更新就可以,不需要考虑 f[u] 有没有可能在 v 子树的情况,因为这样一定不会用 f[u] 去更新 f[v] 
		if(f[v]==-1) f[v]=f[u];
		else if(dist(v,f[v]) > dist(v,f[u])) f[v]=f[u];
		else if(dist(v,f[v]) == dist(v,f[u])) f[v]=min(f[v],f[u]);
		DP2(v);
	}	
	ans[f[u]]++;
}
int check(int u,int x,int y){
	if(dist(u,x) < dist(u,y)) return x;
	else if(dist(u,x) > dist(u,y)) return y;
	else return min(x,y); 
}
void dfs1(int u){   //变量名稍有不同 
	int tmp=Size[u]-1;  //tmp 是处理第一种情况的 
	for(int v:G[u]){
		int x=Get(v,dep[v]-dep[u]-1);
		tmp-=Size[x];
		
		int l=1,r=dep[v]-dep[u]-1,mid,y=v;
		while(l<=r){
			mid=(l+r)>>1;
			if(check(Get(v,mid) , f[v] , f[u])==f[v]) l=mid+1,y=Get(v,mid);
			else r=mid-1;
		}
		ans[f[u]]+=Size[x]-Size[y];
		ans[f[v]]+=Size[y]-Size[v];
		
		dfs1(v);
	}
	ans[f[u]]+=tmp;
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
		}
	}
	T=read();
	while(T--){
		k=read();
		for(int i=1;i<=k;i++) h[i]=a[i]=read(),ans[a[i]]=0,flag[a[i]]=true;
		Build_Virtual_Tree();
		DP1(1);
		DP2(1);
		dfs1(1);  //求全局答案 
		for(int i=1;i<=k;i++) printf("%d ",ans[h[i]]);  //不是 a[i],因为 a[i] 已经被排序了 
		puts("");
		for(int i=1;i<=k;i++) flag[a[i]]=false;
	}
	return 0;
}

95.[HNOI/AHOI2018] 毒瘤

来一道不是那么简单的虚树题。
我保证这是最后一道虚树题了。

题解区还有 ddp广义串并联图 的做法,感兴趣的可以去看。

这个题就是求一个图的独立集个数。
对于一个树的情况,我们有 DP:

  1. \(f_{u,0}=\prod_{v \in son(u)} f_{v,0}+f_{v,1}\)
  2. \(f_{u,1}=\prod_{v \in son(u)} f_{v,0}\)

会发现非树边只有 \(m-n+1\le 11\) 条,对于这些非树边 \((u,v)\) 的两个端点状态只有三种:\((0,0),(0,1),(1,0)\)
而且会发现假设 \(u\) 在树上是 \(v\) 的祖先(无向图搜索树没有横插边),那么我们其实只关心 \(u\) 的状态是什么,即如果 \(u\) 钦定为 \(0\),那对 \(v\) 是没有影响的。
所以 \((0,0)\)\((0,1)\) 可以合并成一个情况,合起来之后就只需要钦定 \(u\) 的状态是什么。
暴力枚举是 \(O(2^{11} n)\)

会发现对 DP 产生影响的只有这 \(22\) 个点,考虑对他们建立虚树。
但虚树上明显不能用上面那个式子,我们来看虚树上的一条边 \((u,v)\) 在原树上对应的那条链:\(u \to x_1 \to x_2 \to ... \to x_k \to v\)\(u\) 是链顶)。
对于这条链上的一个点 \(i\),如果定义 \(g_{i,0}=\prod f_{j,0} + f_{j,1},g_{i,1}=\prod f_{j,0}\),其中 \(j\) 是原树上 \(i\) 不在这条链上的儿子。
那么假设此时我们已经知道 \(v\)\(f\) 值了,那么我们尝试暴力推导出这条链上其他点的 \(f\) 值:
(\(1\))

  • \(f_{x_k,0} = g_{x_k,0} \times (f_{v,0} + f_{v,1})\)
  • \(f_{x_k,1} = g_{x_k,1} \times f_{v,0}\)

(\(2\))

  • \(f_{x_{k-1},0}\)
    \(= g_{x_{k-1},0} \times (f_{x_k,0} + f_{x_k,1})\)
    \(= g_{x_{k-1},0} \times (g_{x_k,0} \times (f_{v,0} + f_{v,1}) + g_{x_k,1} \times f_{v,0})\)
    \(= ( g_{x_{k-1},0} \times (g_{x_k,0} + g_{x_k,1}) ) \times f_{v,0} + (g_{x_{k-1},0} \times g_{x_k,0}) \times f_{v,1}\)
  • \(f_{x_{k-1},1}\)
    \(= g_{x_{k-1},1} \times f_{x_k,0}\)
    \(= g_{x_{k-1},1} \times g_{x_k,0} \times (f_{v,0} + f_{v,1})\)
    \(= g_{x_{k-1},1} \times g_{x_k,0} \times f_{v,0} + g_{x_{k-1},1} \times g_{x_k,0} \times f_{v,1}\)

...

(\(k+1\))

  • \(f_{u,0} = k_{v,0,0}\times f_{v,0} + k_{v,1,0}\times f_{v,1}\)
  • \(f_{u,1} = k_{v,0,1}\times f_{v,0} + k_{v,1,1}\times f_{v,1}\)

会发现不管这个虚树上每个点的状态钦定为什么,一条边 \((u,v)\) 的系数 \(k_{v,0/1,0/1}\) 是不变的。
我们只要算出每条边的系数就可以快速进行虚树上的树形 DP。
求系数其实也不用上面写出来的那么麻烦,程序里面你就模拟展开过程即可。
容易证明暴力对于每一条链 \((u,v)\) 去计算每个链上的点的 \(g\) 值和系数的总复杂度是 \(O(n)\) 的。

但此时会有个问题,就是 \(u\) 可能在虚树上不止一个儿子,即他可能还有一个虚树上的儿子 \(w\),那在处理 \((u,v)\)\(g_u\) 时,会需要用到 \(w\) 那棵子树的 DP 值,这样当去处理 \((u,w)\) 时就算重了。
处理方法也比较简单,把 \(g\) 的定义改一下:
\(g_{i,0}=\prod f_{j,0} + f_{j,1},g_{i,1}=\prod f_{j,0}\),其中 \(j\)\(i\) 在原树上的儿子,且 \(j\) 这棵子树里不存在虚树上的点。
容易发现那些 \(x_1,x_2,...,x_k\)\(g\) 值还是不变的。
但注意的是此时在算 \(k_{v,0/1,0/1}\) 时就不要再把 \(u\)\(g\) 值算进去了,即此时 \(k_{v,0,0}\times f_{v,0} + k_{v,1,0}\times f_{v,1}\) 算出来的并不是 \(f_{u,0}\) 而是 \(f_{x_k,0}\)

那么在虚树上 DP 时,\(f_{u,0}\) 的值就是:

\[\begin{aligned} f_{u,0} &= g_{u,0} \times \prod(f_{x,k,0} + f_{x_k,1}) \\ &= g_{u,0} \times \prod(k_{v,0,0}\times f_{v,0} + k_{v,1,0}\times f_{v,1} +k_{v,0,1}\times f_{v,0} + k_{v,1,1}\times f_{v,1}) \\ \end{aligned} \]

复杂度是:\(O(2^K K)\)\(K=m-n+1\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define int long long 
#define fi first
#define se second
using namespace std;
const int N=1e5+15,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}

int dfn[N],dep[N],fa[N][25],num,cnt;
PII e[N];  //存非树边 
void dfs1(int u){    
	dfn[u]=++num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u][0]) continue;
		if(!dfn[v]){
			fa[v][0]=u,dep[v]=dep[u]+1;
			dfs1(v);
		} 
		else if(dfn[v]<dfn[u]) e[++cnt]={v,u};
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}

int k,h[N],a[N],t[N],c;
bool flag[N];
vector<int> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	k=unique(a+1,a+k+1)-a-1;
	c=0;
	t[++c]=1;
	if(k) t[++c]=a[k];
	for(int i=1;i<k;i++){
		t[++c]=a[i];
		t[++c]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+c+1,[&](int x,int y){return dfn[x]<dfn[y];});
	c=unique(t+1,t+c+1)-t-1;
	for(int i=1;i<=c;i++) flag[t[i]]=true,G[t[i]].clear();

	for(int i=1;i<c;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back(t[i+1]);   
	}
}

void Init(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		add(u,v),add(v,u); 
	} 
	dep[1]=1;
	dfs1(1);
	for(int i=1;i<=20;i++)
		for(int u=1;u<=n;u++)
			fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int i=1;i<=cnt;i++) a[++k]=e[i].fi,a[++k]=e[i].se;
	
	Build_Virtual_Tree(); 
}



int f[N][2],g[N][2];
bool stater[N];  //子树里是否有虚树上的点 
void dfs2(int u){    //预处理 g 数组 
	stater[u]=flag[u];
	f[u][0]=f[u][1]=g[u][0]=g[u][1]=1;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u][0] || fa[v][0]!=u) continue; 
		dfs2(v);
		stater[u]|=stater[v];
		( f[u][0] *= (f[v][0] + f[v][1]) % mod) %= mod;
		( f[u][1] *= f[v][0] ) %= mod;
		if(!stater[v]){
			( g[u][0] *= (f[v][0] + f[v][1]) % mod) %= mod;
			( g[u][1] *= f[v][0] ) %= mod;
		} 
	}
}

int K[N][2][2],tmp[2][2];  //系数,因为上面小写 k 用过了,所以大写 
void solve(int u,int v){   //预处理 K[v][0/1][0/1] 
	K[v][0][0]=K[v][1][1]=1;
	int x=fa[v][0];
	while(x!=u){
		tmp[0][0]=K[v][0][0],tmp[0][1]=K[v][0][1],tmp[1][0]=K[v][1][0],tmp[1][1]=K[v][1][1];
		K[v][0][0] = ( tmp[0][0] + tmp[0][1] ) % mod * g[x][0] % mod;
		K[v][1][0] = ( tmp[1][0] + tmp[1][1] ) % mod * g[x][0] % mod;
		K[v][0][1] = tmp[0][0] * g[x][1] % mod;
		K[v][1][1] = tmp[1][0] * g[x][1] % mod; 
		x=fa[x][0];
	}
}

void Get_DP(){
	dfs2(1);
	for(int u=1;u<=n;u++)
		for(int v:G[u]) solve(u,v);
}



void DP(int u){
	(f[u][0]*=g[u][0])%=mod,(f[u][1]*=g[u][1])%=mod;
	for(int v:G[u]){
		DP(v);
		( f[u][0] *= ( (K[v][0][0] * f[v][0] % mod + K[v][1][0] * f[v][1] % mod) % mod + ( K[v][0][1] * f[v][0] % mod + K[v][1][1] * f[v][1] % mod) % mod ) % mod ) %= mod;
		( f[u][1] *= (K[v][0][0] * f[v][0] % mod + K[v][1][0] * f[v][1] % mod) % mod ) %= mod;
	}
}
void work(){
	int ans=0;
	for(int S=0;S<(1<<cnt);S++){
		for(int i=1;i<=c;i++) f[t[i]][0]=f[t[i]][1]=1;
		for(int i=1;i<=cnt;i++)
			if(S>>(i-1)&1) f[e[i].fi][0]=0,f[e[i].se][1]=0;
			else f[e[i].fi][1]=0;
		DP(1);
		( ans += (f[1][0] + f[1][1]) % mod ) %= mod;
	}
	printf("%lld\n",ans);
}
signed main(){
	Init();
	Get_DP(); 
	work();
	return 0;
}

96.[HNOI2004] L 语言

先看朴素的 DP,设 \(f_i\) 表示前缀 \(i\) 可不可以被理解,转移是:\(f_j \to f_i\),需要满足 \(s[j+1,i]\) 是一个单词。

考虑对字典里的模式串建出 AC 自动机。
如果 \(t\) 的前缀 \(i\) 在 fail 树上代表点 \(u\),并且他的一个祖先 \(v\) 是一个模式串的终止节点。
\(v\) 代表的模式串长度为 \(len\),则 \(i\) 的长度为 \(len\) 的后缀就是一个模式串。
也就意味着 \(f_{i-len}\) 可以转移到 \(f_i\)
如果在 fail 树上暴力地跳,并挨个判断每个祖先,那么是 \(O(m|s||t|)\) 的。

注意到 \(|s|\le 20\),也就是说所有以 \(u\) 结尾的模式串的长度是可以状压的,我们记在 \(g\) 数组里面,即 \(g_u\) 的第 \(k\) 位是 \(1\) 代表 \(u\) 结尾有一个长度为 \(k\) 的模式串。
又因为能转移到 \(f_i\)\(f_j\) 必定满足 \(j\ge i-|s|\),所以我们也可以对 \(f\) 数组状压,记在 \(h\) 数组里,即如果 \(h_i\) 的第 \(k\) 位是 \(1\) 那么代表 \(f_{i-k}=true\)
所以 \(f_i = [(g_u | h_i) \ne 0]\)\(u\) 是前缀 \(i\) 在 fail 树上代表的点。
其中 \(g\) 可以预处理,\(h\) 数组在转移的时候维护就可以了。

复杂度是:\(O(\sum |s| + \sum |t|)\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=500+5,M=2e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
string s;
int n,T,tot,t[N][30],len[N],flag[N];
void insert(string s){  
	int p=0,dep=0;
	for(int i=0;i<s.size();i++){
		int ch=s[i]-'a';
		if(!t[p][ch]) t[p][ch]=++tot;
		p=t[p][ch],++dep;
	}
	flag[p]=1;
	len[p]=dep;
}

int fail[N];
vector<int> G[N];
void getfail(){
	queue<int> q;
	for(int i=0;i<26;i++){
		if(t[0][i]) q.push(t[0][i]);
	}
	while(q.size()){
		int u=q.front(); q.pop();
		for(int i=0;i<26;i++){
			if(t[u][i])
				fail[t[u][i]]=t[fail[u]][i],q.push(t[u][i]);
			else
				t[u][i]=t[fail[u]][i];
		}
	}
	for(int i=1;i<=tot;i++) G[fail[i]].push_back(i);
}

int g[M];
bool f[M];
void dfs(int u){
	for(int v:G[u]){
		g[v]=g[u] | (flag[v] * (1<<len[v])); 
		dfs(v);
	} 
}

void Init(){
	memset(f,false,sizeof f);
} 
signed main(){
	ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
	cin>>n>>T;
	for(int i=1;i<=n;i++){
		cin>>s;
		insert(s);
	}
	getfail();
	dfs(0);
	while(T--){
		Init();
		cin>>s; s=' '+s;
		int p=0,h=1,ans=0;
		for(int i=1;i<s.size();i++){
			p=t[p][s[i]-'a'];
			h<<=1;
			if((h&g[p])!=0) f[i]=true,ans=max(ans,i);
			h|=f[i];
		}
		printf("%d\n",ans);
	}
	return 0;
}

97.[ARC104C] Fair Elevator

形式化题意:
一个长度为 \(2n\) 的数轴,有 \(n\) 个区间,每个点只会作为一个区间的端点。
当两个区间相交时,那么他们的长度也必须相等,现在给了你若干个区间和某些区间的其中几个端点。
求是否有合法的分配方案。

首先如果有一个区间是 \([l,r]\),那么必然有这些区间:\([l+1,r+1] , [l+2,r+2] , ... , [r-1,2r-l-1]\)
即最后的数轴可以被分成若干个独立的小段,每个小段的分配方案都跟上面一样。
那么因为互相独立所以就可以比较简单的 DP 了。
\(f_i\) 表示前 \(i\) 段是否可行。
转移为:\(f_j \to f_i\),需要满足 \([j+1,i]\) 可以被分配成形如:\([l+1,r+1] , [l+2,r+2] , ... , [r-1,2r-l-1]\)
具体 check 方法见代码,挺好理解的。

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],b[N],cnt[N],f[N];
PII op[N];
bool check(int l,int r){
	int mid=(l+r)/2,len=(r-l+1)/2;
	for(int i=l;i<=mid;i++){
		if(op[i].fi && op[i+len].fi && op[i].fi!=op[i+len].fi) return false;   //如果这两个端点已经被占了,但是不是同一个区间的就不行
		if( (op[i].fi && op[i].se==1) || (op[i+len].fi && op[i+len].se==0) ) return false;  //有一个被占了,但是这个点应该是左端点现在确实右端点(或反过来) 
	}
	return true;
}
signed main(){
	n=read();
	for(int i=1;i<=n;i++){
		a[i]=read(),b[i]=read();
		if(~a[i]) cnt[a[i]]++,op[a[i]]={i,0};
		if(~b[i]) cnt[b[i]]++,op[b[i]]={i,1};
	}
	for(int i=1;i<=2*n;i++)
		if(cnt[i]>1){
			puts("No");
			return 0;
		}
	f[0]=true;
	for(int i=2;i<=2*n;i+=2){   //注意到每一段区间的长度和一定是偶数 
		for(int j=i-2;j>=0;j-=2)
			f[i]|=(f[j] && check(j+1,i));
	}
	puts(f[2*n]?"Yes":"No");
	return 0;
}

98.[ARC104D] Multiset Mean

把每个数 \(-x\),那么就是求和为 \(0\) 的方案数,此时可选数的值域是 \([1-x,n-x]\)
把它分成 \(3\) 段:\([1-x,-1],0,[1,n-x]\)
\(0\) 可以随便选,方案数是 \(k+1\)
现在相当于第一部分和第三部分的绝对值要相等。
预处理 \(f_{i,j}\) 表示用值域 \([1,i]\) 凑出 \(j\) 的方案数,那么:\(ans = (k+1) \times \sum_{j\le V} f_{x-1,j}\times f_{n-x,j}\)
其中 \(V\) 最大是 \(\frac{n(n+1)k}{2}\)

这里多重背包计算的是方案数,所以就不用单调队列优化了,直接前缀和优化即可。
复杂度是 \(O(n^3k)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e2+5,M=500005;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,K,mod,c[N],f[N][M],pre[M],m; 
signed main(){
	n=read(),K=read(),mod=read();
	m=n*(n-1)/2*K;
	f[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<i;j++){
			for(int k=0;j+k*i<=m;k++) pre[k]=0;
			for(int k=0;j+k*i<=m;k++){
				pre[k]=f[i-1][j+k*i];
				if(k>0) (pre[k]+=pre[k-1])%=mod;
				f[i][j+k*i]=(k-K-1<0)?(pre[k]):((pre[k] - pre[k-K-1] + mod)%mod);
			}
		}
	}
	for(int x=1;x<=n;x++){
		int ans=0;
		for(int j=0;j<=m;j++){
			(ans += f[x-1][j]*f[n-x][j]%mod) %= mod;
		}
		printf("%lld\n",(ans*(K+1)%mod -1 + mod)%mod);
	}
	return 0;
}

99.[ARC104E] Random LIS

这年头 \(n\le 6\) 的数据真不多见。

首先根据期望的定义答案就是所有可能的 LIS 的和去除总方案数 \(\prod_{i=1}^1 a_i\)

因为 \(n\) 实在是太小了,所以我们可以大胆地 \(O(n^n)\) 去枚举每个数的排名,因为可以相等所以不是 \(O(n!)\)说实话赛时我都没去想这种复杂度
然后先求出此时的 LIS,现在就是要统计有多少满足当前排名数组的方案了。
对于排名相同的数,我们可以缩成一个数,其中这个数的值域是它们值域的交。
现在我们得到了 \(m\) 个数,以及他们排名 \(rk\) 数组。
把他们按照 \(rk\) 排序后,就得到了一个长度为 \(m\) 的上升序列,其中每个数有一个值域 \(b_i\)
现在问题变成:有多少个长度为 \(m\) 的严格单调上升的序列 \(c\),并且 \(c_i \in [1,b_i]\)
虽然 \(b_i\) 很大,但是它们在数轴上分出来的值域段却很少,比如 \(b\) 如果是: \(6,4,2,8\),那么就把值域分成了 \([1,2],[3,4],[5,6],[7,8]\) 四段。
考虑 DP,设 \(f_{i,j}\) 表示前 \(i\) 个数,其中第 \(i\) 个数的值在第 \(j\) 段的方案数,转移时枚举 \(k\)\(x\), 表示 \(c[k+1,i]\) 这些数全都放在第 \(j\) 段,\(c_k\) 放在第 \(x\) 段:
\(f_{i,j}=\sum f_{k,x} \times C_{len_j}^{i-k}\)
转移系数 \(C_{len_j}^{i-k}\) 即从第 \(j\) 段选出 \(i-k\) 个位置。
注意还要判断 \([k+1,i]\) 这些数是否都可以取到第 \(j\) 段,通过值域段的形成过程容易证明这些数的值域要么与第 \(j\) 段包含,要么相离,绝不可能相交的,所以只用看是否包含即可。
所有过程全都暴力即可,\(O(n^5)\) 的 DP。

时间复杂度 \(O(n^{n+5})\) 实际根本跑不满。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=10+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],sum,INV=1;
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		b>>=1,(a*=a)%=mod;
	}	
	return ans;
}
int rk[N],dp[N],t[N],b[100],len[N],f[N][N];
int C(int n,int m){  //这个要暴力算 
	if(m>n) return 0; 
	int ans=1;
	for(int i=n-m+1;i<=n;i++) (ans*=i)%=mod;
	for(int i=1;i<=m;i++) (ans*=quick_power(i,mod-2))%=mod;
	return ans; 
}
bool check(int l,int r,int id){
	for(int i=l;i<=r;i++) if(t[i]<b[id]) return false;
	return true;
}
int solve(int m){
	for(int i=1;i<=m;i++) b[i]=LLONG_MAX;
	for(int i=1;i<=n;i++) b[rk[i]]=min(b[rk[i]],a[i]);
	for(int i=1;i<=m;i++) t[i]=b[i];
	sort(b+1,b+m+1);
	int cnt=unique(b+1,b+m+1)-b-1;
	for(int i=1;i<=cnt;i++) len[i]=b[i]-b[i-1];
	memset(f,0,sizeof f);
	f[0][0]=1;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=cnt;j++){
			for(int k=0;k<i;k++){
				if(!check(k+1,i,j)) continue;
				for(int x=0;x<j;x++){
					(f[i][j] += f[k][x] * C(len[j],i-k) % mod) %= mod;
				}
			}
		}
	}
	int res=0;
	for(int j=0;j<=cnt;j++) (res+=f[m][j])%=mod;
	return res;
}
void dfs(int u){
	if(u==n+1){
		int maxrk=0; 
		bool flag[8]; 
		memset(flag,0,sizeof flag);
		for(int i=1;i<=n;i++) flag[rk[i]]=true,maxrk=max(maxrk,rk[i]);
		for(int i=1;i<=n;i++) if(!flag[i] && flag[i+1]) return;  //排名数组不合法 
		int LIS=0;
		for(int i=1;i<=n;i++){
			dp[i]=1;
			for(int j=1;j<=i-1;j++){
				if(rk[j]<rk[i]) dp[i]=max(dp[i],dp[j]+1);
			}
			LIS=max(LIS,dp[i]);
		}
		(sum+=LIS*solve(maxrk)%mod)%=mod;
		return;
	}
	for(int i=1;i<=n;i++){
		rk[u]=i;
		dfs(u+1);
	}
}
signed main(){
	n=read();
	for(int i=1;i<=n;i++) a[i]=read(),(INV*=quick_power(a[i],mod-2))%=mod;
	dfs(1);
	printf("%lld\n",sum*INV%mod);
	return 0;
}

100.无题

第 100 个题就简单点吧。

题面

给你一棵 \(n\) 个点的树和一个数 \(x\),点有点权 \(a_i\),问有多少种把树分成若干连通块的方法,使得每一个连通块内的点的异或和都是 \(x\)
答案对 \(998244353\) 取模。
数据范围: \(n\le 10^6,1\le x,a_i \le 10^9\)

题解

一个子树如果能恰好分成若干合法的连通块,那么子树的异或和一定等于 \(0 / x\)

而一个子树假设划分完之后还剩一些东西,那这些东西一定在上面,且他们的异或和只能是 \(sum / sum\oplus x\)
其中 \(sum\) 是子树的异或和。

那么设 \(f_{u,0/1}\) 表示剩余的点的异或和是 \(sum / sum \oplus x\) 的方案数。
转移时就是个普通的树形背包。

\(O(n)\)

code

#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e6+5,mod=998244353;
inline int read() {
	int w = 1, s = 0;
	char c = getchar();
	for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
	for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
	return s * w;
}
int n,x,a[N];
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v) {
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}

int f[N][2];
int dfs(int u,int fa){
	int sum=a[u];
	f[u][0]=1,f[u][1]=0;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa) continue;
		sum^=dfs(v,u);
		int tmp[2];
		tmp[0]=(f[u][0]*f[v][0]%mod + f[u][1]*f[v][1]%mod)%mod;
		tmp[1]=(f[u][1]*f[v][0]%mod + f[u][0]*f[v][1]%mod)%mod;
		f[u][0]=tmp[0],f[u][1]=tmp[1];
	}
	int tmp[2];
	tmp[0]=f[u][0],tmp[1]=f[u][1];
	if(sum==x) (f[u][1]+=tmp[0])%=mod;  //可以没有剩余,即不往上上传
	if(sum==0) (f[u][0]+=tmp[1])%=mod; 
	return sum;
}

int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		b>>=1,(a*=a)%=mod;
	}
	return ans;
} 
signed main() {
	freopen("town.in","r",stdin);
	freopen("town.out","w",stdout);
	n=read(),x=read();
	for(int i=1; i<=n; i++) a[i]=read();
	for(int i=1; i<n; i++) {
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	
	int sum=dfs(1,0),ans;
	if(sum==x) ans=f[1][0];
	else if(sum==0) ans=f[1][1];
	else ans=0;
	
	printf("%lld\n",ans);
	return 0;
}

从这题开始组合数就不用 \(C_n^m\) 改用 \(\binom{n}{m}\) 了。


101. Counting swaps

容易知道把他变成有序的最小操作数是 \(\sum_{i=1}^{cnt} siz_i-1=n-cnt\)\(siz_i\) 表示第 \(i\) 个置换环的长度,\(cnt\) 是置换环个数。

\(f_n\) 表示把长度为 \(n\) 的环分解成 \(n\) 个自环的方案数。
那么枚举第一次操作后他分成的两个小环的大小 \(x,y\)
如果 \(x=y\) 那么有 \(\frac{n}{2}\) 种操作方法把他分成 \(x,y\) 两个环 ; 否则这一步的方案数为 \(n\),我们记作 \(T(x,y)\)
然后可以得到 \(f_n=\sum_{x+y=n} T(x,y) \times f_x \times f_y \times \binom{n-2}{x-1}\)

则答案 \(ans=\frac{(n-cnt)!}{\prod (siz_i-1)!} \times \prod f_{siz_i}\)

注意到此时的瓶颈在求 \(f\)\(O(n^2)\) 的。
打表可以发现 \(f_n=n^{n-2}\),所以可以优化成 \(O(n \log n)\)

当然我们需要证明。
会发现 \(n^{n-2}\) 是经典的有标号无根树的个数,于是考虑在操作序列和有标号无根树之间建立双射。
如果交换了 \(x,y\) 就在 \(x,y\) 之间连一条边,那么 \(n-1\) 次操作后就可以得到一棵 \(n\) 个点的树。
但注意到你正着推并没有双射关系,因为对于一个置换环,相同的操作集合,只要操作顺序不同,这两种操作方案就是不同的,但是他们得到的无根树是一样的。
那么我们就倒着考虑,假设我们得到了最后那棵无根树,并且钦定了每条边之间的顺序(一共有 \(n^{n-2}\times (n-1)!\) 种情况),那么我们倒着执行所有操作,每次操作变成合并两个环,我们会得到一个唯一确定的环。而这个操作序列同样是那个环的一个合法操作方案。
所以所有长度为 \(n\) 的置换环的方案个数是 \(n^{n-2}\times (n-1)!\),显然每个置换环的方案数都是一样的,而长度为 \(n\) 的置换环一共有 \((n-1)!\) 个(就是一个圆排列数)。
所以每个长度为 \(n\) 的置换环的方案数都是 \(\frac{n^{n-2}\times (n-1)!}{(n-1)!}=n^{n-2}\)

该证明参考自:这篇题解

这题虽然最终做法不是 DP,但是前半部分是个计数 DP 题的经典思路,并且结论也应该牢记。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=1e9+9;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],dis[N],fact[N],q[N],inv[N],f[N],ans;
int Dis(int x){
	return lower_bound(dis+1,dis+n+1,x)-dis;
}
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		b>>=1,(a*=a)%=mod; 
	}
	return ans;
}
//int Swap(int x,int y){
//	return (x==y)?((x+y)/2):(x+y);
//}
int fa[N],Size[N];
int get(int x){
	return x==fa[x]?x:(fa[x]=get(fa[x])); 
}
void merge(int x,int y){
	x=get(x),y=get(y);
	if(x==y) return;
	Size[y]+=Size[x];
	fa[x]=y;	
}
signed main(){
//	freopen("perm.in","r",stdin);
//	freopen("perm.out","w",stdout);
	int T=read();
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	q[0]=1;
	for(int i=1;i<N;i++) q[i]=q[i-1]*inv[i]%mod;
	while(T--){
		n=read();
		for(int i=1;i<=n;i++) fa[i]=i,Size[i]=1;
		for(int i=1;i<=n;i++) dis[i]=a[i]=read();
		sort(dis+1,dis+n+1);
		for(int i=1;i<=n;i++) merge(i,Dis(a[i]));
		
		f[1]=1;
		for(int i=2;i<=n;i++){
	//		for(int x=1;x<=i/2;x++){
	//			int y=i-x;
	//			(f[i]+=Swap(x,y) * f[x] % mod * f[y] % mod * fact[i-2] % mod * q[x-1] % mod * q[y-1] % mod)%=mod; 
	//		}
	//		cout<<i<<':'<<f[i]<<'\n';
			f[i]=quick_power(i,i-2);
		}
		
		ans=1;
		int sum=0;
		for(int i=1;i<=n;i++){
			if(get(i) == i){
				sum+=Size[i]-1;
				(ans *= f[Size[i]]) %= mod;
				(ans *= q[Size[i]-1]) %= mod;
			}
		}
		(ans *= fact[sum]) %= mod;
		printf("%lld\n",ans);
	}
	
	return 0;
}

102.Jellyfish and EVA

翻译可能有点不清楚:
就是一个点 \(u\) 在选择了 \(v\) 但是 \((u,v)(u,w)\) 不同时,在销毁这两条边之后会继续选下一个 \(v'\),直到 \(v'=w'\)

首先对于一个点的所有出边我们肯定是钦定一个顺序去选,那我们首先我们需要预处理一个 \(f_{i,j}\) 表示假如我们有 \(i\) 条出边,成功选到第 \(j\) 条的概率是多少。
初值:\(f_{i,1}=\frac{1}{i}\)
对于其他 \(f_{i,j}\) 考虑当我选第一条边时随机到了哪一条边。
首先它一定不能随机到第一条边(不然在第一条边我就直接走掉了),假设随机到了边 \(x\)

  1. \(1<x<j\):那么现在第 \(j\) 条边变成了第 \(j-2\) 条边,\(f_{i-2,j-2} \times \frac{j-2}{i} \to f_{i,j}\)
  2. \(j<x\le i\):那么现在第 \(j\) 条边变成了第 \(j-1\) 条边,\(f_{i-2,j-1} \times \frac{i-j}{i} \to f_{i,j}\)

\(g_u\) 表示从点 \(u\) 走到 \(n\) 的概率,那么因为题目中说每一条边都满足 \(u<v\),所以我们倒着转移。
现在相当于是把 \(u\) 的所有出点 \(v\)\(g_v\)\(f_{deg_u,j}\) 去配对,问乘积之和最大是多少。
根据经典结论,把他们分别升序排序然后依次匹配就是最优的。
(事实上 \(j\) 越大 \(f_{i,j}\) 越小,但你排序了复杂度也不会错)。

\(O(n^2 + m \log m)\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,M=2e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,deg[N];
vector<int> G[N];
double f[N][N],g[N];
void add(int u,int v){
	deg[u]++;
	G[u].push_back(v);
}
void Init(){
	for(int i=1;i<=n;i++) G[i].clear(),deg[i]=0;
} 
signed main(){
	int T=read();
	while(T--){
		n=read(),m=read();
		Init();
		for(int i=1;i<=m;i++){
			int u=read(),v=read();
			add(u,v);
		}
		for(int i=0;i<=n;i++) f[i][0]=0,g[i]=0;
		for(int i=1;i<=n;i++){
			f[i][1]=1.0/i;
			for(int j=2;j<=i;j++) f[i][j]=1.0*f[i-2][j-2]*(j-2)/i + 1.0*f[i-2][j-1]*(i-j)/i;
		}
		g[n]=1.0;
		for(int i=n;i>=1;i--){
			sort(G[i].begin(),G[i].end(),[&](int x,int y){return g[x]>g[y];});
			for(int j=1;j<=deg[i];j++) g[i]+=g[G[i][j-1]]*f[deg[i]][j];
		}
		printf("%.12lf\n",g[1]);
	}
	return 0;
}

103.Jellyfish and Miku

用的是第一篇题解的思路。

\(f_i\) 表示 \(i\) 走到 \(n\) 的期望步数。
那么根据题意,可以得到:

\[f_i = \begin{cases} 1 + f_1 & i=0 \\ 1 + \frac{a_i}{a_i+a_{i+1}} \times f_{i-1} + \frac{a_{i+1}}{a_i+a_{i+1}} \times f_{i+1} & 1 \le i <n \\ 0 & i=n \end{cases} \]

化简第二个式子得到:
\(a_{i+1} \times (f_i - f_{i+1}) = a_{i} \times (f_{i-1} - f_i) + a_i + a_{i+1}\)
\(g_i=f_{i-1}-f_i\),那么就等价于:
\(a_{i+1} \times g_{i+1} = a_i \times g_i + a_i + a_{i+1}\)

于是得到 \(g\) 的递推式:

\[g_i = \begin{cases} 1 & i=1 \\ \frac{a_{i-1}}{a_i} g_{i-1} + \frac{a_{i-1}}{a_i} +1 & 1<i\le n \end{cases} \]

手推可以得到通项公式:

\[g_i = 1 + 2 \times \frac{1}{a_i} \times \sum_{j=1}^{i-1} a_j \]

答案是 \(f_0\),用 \(g\) 表示他:

\[\begin{aligned} f_0 &= f_0 - f_n \\ &= \sum_{i=1}^n g_i \\ &= \sum_{i=1}^n (1 + 2 \times \frac{1}{a_i} \times \sum_{j=1}^{i-1} a_j) \\ &= n + 2 \times (\sum_{i=1}^n \frac{1}{a_i} \sum_{j=1}^{i-1} a_j) \\ \end{aligned} \]

于是问题变成最小化 \(\sum_{i=1}^n \frac{1}{a_i} \sum_{j=1}^{i-1} a_j\)
这个问题可以 DP,设 \(dp_{i,s}\) 表示 \(\sum_{j=1}^i a_j =s\) 时,前 \(i\) 个数的上面那个式子的值。
转移:\(dp_{i,s} + \frac{s}{j} \to dp_{i+1,s+j}\)
复杂度是 \(O(m^2n)\),过不了。

此时我们需要一个结论:\(a_i\) 一定是单调不降的。

证明:邻项交换可证。

所以 \(s+k(n-i) \le m\),于是复杂度变成调和级数,\(O(m^2 \log n)\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=3e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m;
double f[N][N]; 
signed main(){
	n=read(),m=read();
	for(int i=0;i<=n;i++)
		for(int s=0;s<=m;s++)
			f[i][s]=1e9;
	f[0][0]=0;
	for(int i=0;i<n;i++){
		for(int s=0;s<=m;s++){
			for(int k=1;k*(n-i)+s<=m;k++){
				f[i+1][s+k]=min(f[i+1][s+k],f[i][s]+1.0*s/k);
			}
		}
	}
	double ans=1e9;
	for(int s=0;s<=m;s++) ans=min(ans,f[n][s]);
	printf("%.12lf",2.0*ans+1.0*n);
	return 0;
}

104.Good Contest

这个题就是 "99.[ARC104E] Random LIS" 在爆搜完之后数方案数的子问题。
只不过值域从 \([1,r]\) 变成了 \([l,r]\),然后从严格单增变成不严格单降。

前面是一样的,设 \(f_{i,j}\) 表示考虑前 \(i\) 个数,其中第 \(i\) 个数在第 \(j\) 段(这里把段从大到小排序)的方案数。
考虑从 \(f_{k,x}\) 转移过来,即 \(a[k+1,i]\) 都在第 \(j\) 段,同样的需要判断他们的值域是否和第 \(j\) 段有交。
现在相当于是要求在第 \(j\) 段放这 \(i-k\) 个数的方案数,注意到它们的大小关系已经定了,我们选位置就可以了。
但是因为题目描述是单调不增而不是单调下降,所以一个位置可以放多个数,算这个也很简单:
假设这段有 \(len\) 个位置,我们要放进 \(m=i-k\) 个位置。
那么相当于是要解一个形如:\(x_1+x_2+...+x_{len}=m\) 的方程。
求他的非负整数解的个数,根据插板法答案是 \(\binom{len+m-1}{len-1}=\binom{len+m-1}{m}\)

因为这个组合数很大不能预处理,所以求组合数有个 \(O(n)\),然后枚举 \(x\) 用前缀和优化掉即可。
时间复杂度 \(O(n^4)\)

code

#include<bits/stdc++.h>
#define int long long 
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=50+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,l[N],r[N],f[N][N<<2],pre[N][N<<2],inv[N],sum;
struct P{
	int pos,op;
}a[N<<1];
int cnt;
PII range[N<<1];  //左闭右开 
bool check(int L,int R,int id){
	for(int i=L;i<=R;i++){
		if(max(l[i],range[id].fi)>min(r[i],range[id].se-1)) return false;
	}
	return true;
}
int C(int n,int m){  //m 比较小 
	if(m>n) return 0;
	int res=1;
	for(int i=n-m+1;i<=n;i++) res=res*i%mod;
	for(int i=1;i<=m;i++) res=res*inv[i]%mod;
	return res; 
}
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		b>>=1,(a*=a)%=mod;
	}
	return ans;
}
signed main(){
	n=read();
	inv[1]=1; 
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	
	sum=1;
	for(int i=1;i<=n;i++){
		l[i]=read(),r[i]=read(),(sum*=(r[i]-l[i]+1))%=mod;
		a[i]={l[i],1},a[i+n]={r[i]+1,-1};
	} 
	sort(a+1,a+2*n+1,[&](P x,P y){return x.pos<y.pos;});
	for(int i=1,tmp=0;i<=2*n;i++){  //我想不到更好的搞出区间的方法了 
		if(tmp>0) range[++cnt]={a[i-1].pos,a[i].pos};
		tmp+=a[i].op;
	}
 	reverse(range+1,range+cnt+1);
 	
	f[0][0]=1;
	for(int i=0;i<=cnt;i++) pre[0][i]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=cnt;j++){
			for(int k=0;k<i;k++){
				if(!check(k+1,i,j)) continue;	
				int len=range[j].se-range[j].fi,num=i-k;
				(f[i][j]+=pre[k][j-1]*C(len+num-1,num)%mod)%=mod;
			}
		}
		for(int j=1;j<=cnt;j++) pre[i][j]=(pre[i][j-1]+f[i][j])%mod;
	}
	
	printf("%lld\n",pre[n][cnt]*quick_power(sum,mod-2)%mod);
	return 0;
} 

105. Tenzing and Random Operations

这个题有两种做法,一种是组合意义,一种是代数法,这里讲前者。

考虑 \(\prod a_i\) 的组合意义,相当于是从 \(i\) 走到 \(i+1\)\(a_i\) 种方案,求 \(1\) 走到 \(n+1\) 的方案数。
修改意味着什么,相当于是在一个位置 \(i\) 放了一个工具,走到 \(i\) 之后就拥有了这个工具,在之后的每一步 \((u,u+1)\) 你都可以使用这个工具使你走到 \(u+1\) 多了 \(v\) 个方案。
注意:这个工具不是一次性的,可以用多次。

那么虽然 \(m\) 很大,但是所有用过的工具肯定只有 \(n\) 种,于是考虑 \(O(n^2)\) 的 DP。
\(f_{i,j}\) 表示走到 \(i\) ,我用了 \(j\) 种工具(注意不是 \(j\) 次工具)的方案数,转移有:

  1. \(a_i\)\(f_{i,j} \times a_i \to f_{i+1,j}\)
  2. 用先前用过的工具:\(f_{i,j}\times j\times v \to f_{i+1,j}\)
  3. 用一个我先前没用过的工具,先看他是剩下 \(m-j\) 个中的哪一个,再看他放在了前 \(i\) 个位置的哪一个: \(f_{i,j}\times (m-j)\times i\times v \to f_{i+1,j+1}\)

答案就是 \(\frac{\sum_{i=0}^n f_{n+1,i} \times n^{m-i}}{n^m} = \sum_{i=0}^n \frac{f_{n+1,i}}{n^i}\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=5e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,v,a[N],f[N][N];
int qp(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1;
	}
	return ans;
}
signed main(){
	n=read(),m=read(),v=read();
	for(int i=1;i<=n;i++) a[i]=read();
	f[1][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=i;j++){
			(f[i+1][j] += f[i][j] * a[i] % mod)%=mod;
			(f[i+1][j] += f[i][j] * v % mod * j % mod)%=mod;
			(f[i+1][j+1] += f[i][j] * (m-j) % mod * i % mod * v % mod)%=mod;
		}
	}
	int ans=0;
	for(int i=0;i<=n;i++) (ans+=f[n+1][i]*qp(qp(n,i),mod-2)%mod)%=mod;
	printf("%lld\n",ans);
	return 0;
}

106.Tenzing and Random Real Numbers

一个trick:右边的 \(1\) 很难受,考虑令 \(y_i=x_i-0.5\),那么限制变为:

  • \(y_i+y_j\le 0\)
  • \(y_i+y_j\ge 0\)

其中 \(y_i\)\([-0.5,0.5]\) 等概率随机。
首先这个 = 直接去掉对答案是没有影响的,因为在一个无限大的集合里随机一个数随机到一个确定的数的概率是 \(0\)
考虑 \(y_i+y_j\) 的符号跟什么有关,容易证明他的符号就是绝对值大的那个数的符号。
所以一个条件相当于限制了绝对值大的那个数的符号。
而一个 \(y\) 取到正负的概率均为 \(1/2\),所以直接合法方案数/总方案数即为概率。

现在我们只需要给 \(y\) 确定一个顺序(按绝对值从大到小排),并确定每个数的符号。
考虑根据值域从大到小状压 DP,每次新加进来的一数定放在末尾,枚举它的符号是什么并判断是否满足所有条件即可。
总方案数就是 \(n! 2^n\)

暴力 check 复杂度就是 \(O(2^n m)\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second 
//#define int long long 
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,f[(1<<20)+5];
vector<PII> G[N];
int inv[N];
signed main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int op=read(),u=read(),v=read();
		G[u].push_back({v,op}),G[v].push_back({u,op});
	}
	f[0]=1;
	for(int s=0;s<(1<<n);s++){
		for(int i=1;i<=n;i++){  //枚举下一个放啥 
			if(s>>(i-1)&1) continue;
			//我们只需要判断以他作为绝对值最大的数的限制是否满足即可
			for(int opt=0;opt<=1;opt++){
				bool flag=true;
				for(PII limit:G[i]){
					int j=limit.fi,op=limit.se;
					if(s>>(j-1)&1) continue;
					if(op!=opt){
						flag=false;
						break;
					} 
				} 
				if(flag) (f[s|(1<<(i-1))]+=f[s])%=mod;
			}
		}
	}
	
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	
	int ans=f[(1<<n)-1];
	//总方案数为 n!*(2^n) 先确定顺序再确定正负号 
	for(int i=1;i<=n;i++) ans=1ll*ans*inv[i]%mod;
	for(int i=1;i<=n;i++) ans=1ll*ans*inv[2]%mod;
	printf("%d\n",ans);
	return 0;
}

107.Sports Betting

几个题面注意点:\(i\) 打败了 \(j\),不一定代表 \(j\) 没打败 \(i\)

根据期望线性性,我们只要算出每个人成为 Winner 的概率加起来即可。

起手状压 DP:\(f_{i,S}\) 表示 \(i\) 打败集合 \(S\) 所有人的概率,特殊地 \(S\) 一定包含 \(i\)
考虑容斥,用 \(1-\) 不合法的概率。
\(P(\text{不合法概率})=\sum f_{i,T}\times g_{S/T,T}\)。(\(S/T\) 是补集的意思。)
其中 \(T\)\(S\) 的一个包含 \(i\) 的子集,\(g_{S,T}\) 表示集合 \(S\) 的所有人都打败了集合 \(T\) 的人,且集合 \(T\) 的人没一个打败 \(S\) 的人的概率。
\(O(n^2)\) 直接算 \(g\) 的话求解一个人的概率的复杂度是 \(O(3^nn^2)\)
总复杂度是 \(O(3^nn^3)\) 过不去。

优化求 \(g\) 的方法,先 \(O(n^22^n)\) 预处理出 \(h_{i,S}\) 表示 \(i\) 打败 \(S\) 中所有人,且 \(S\) 中全都没打败 \(i\) 的概率。
这样就可以 \(O(n)\)\(g\) 了。
总时间复杂度 \(O(3^nn^2)\)

因为 \(S,T\) 还有要包含 \(i\) 的限制,所以跑不满,\(4s\) 随便过的。

code

#include<bits/stdc++.h>
#define int long long  
using namespace std;
const int N=2e6+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],inv[N],ans;
int h[16][(1<<14)+5],f[16][(1<<14)+5];
int solve(int x){
	for(int s=0;s<(1<<n);s++){
		if(!(s>>(x-1)&1)) continue;
		for(int t=s;t;t=(t-1)&s){
			if(!(t>>(x-1)&1)) continue;
			int tmp=s^t,g=1;
			for(int i=1;i<=n;i++){
				if(tmp>>(i-1)&1) g=g*h[i][t]%mod;
			}
			f[x][s]=(f[x][s]+g*f[x][t]%mod)%mod;
		}
		f[x][s]=(1-f[x][s]+mod)%mod;
	}
	return f[x][(1<<n)-1];
} 
signed main(){
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	
	for(int i=1;i<=n;i++){
		for(int s=0;s<(1<<n);s++){
			if(s>>(i-1)&1) continue;  //不能包含 i
			h[i][s]=1;
			for(int j=1;j<=n;j++){
				if(s>>(j-1)&1) h[i][s]=h[i][s] * a[i] % mod * inv[a[i]+a[j]] % mod;
			}
		}
	}
	for(int i=1;i<=n;i++) ans=(ans+solve(i))%mod;

	printf("%lld\n",ans);	
	return 0;
}

108.找行李

题面


数据范围: \(n,m\le 200,1\le a_i,b_i \le 500\)\(a_i,b_i\) 互不相同。

题解

首先一个经典技巧是:\(f_d\) 表示答案 \(\ge d\) 的方案数,那么差分一下可以得到 \(ans=\sum_{i=1}^{+\infty} f_i\)(实际上 \(d\) 只需要枚举到值域最大值)。
对于一个 \(d\),每个人能选择的箱子是一段前缀,并且前面的人的可选箱子是后面的人的可选箱子的子集。
于是考虑 DP,\(f_{i,j}\) 表示前 \(i\) 个人,选了 \(j\) 个箱子的方案数(\(d\) 确定),那么假设第 \(i\) 个人可选的箱子有 \(k\) 个:\(f_{i,j} = f_{i-1,j} + f_{i-1,j-1}\times (k-(j-1))\)
意义为,要么第 \(i\) 个人不匹配,要么匹配一个箱子,可匹配的箱子有 \(k-(j-1)\) 个(因为前面 \(i-1\) 个人匹配掉了 \(j-1\) 个)。
\(O(n^2V)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=500+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,a[N],b[N],f[N][N],ans;
signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=m;i++) b[i]=read();
	sort(a+1,a+n+1),sort(b+1,b+m+1);
	for(int d=1;d<=500;d++){
		memset(f,0,sizeof f);
		f[0][0]=1;
		for(int i=1,k=0;i<=m;i++){
			for(int j=0;j<=min(i,n);j++){
				while(k+1<=n && b[i]-a[k+1]>=d) k++;
				f[i][j]=f[i-1][j];
				if(k>=j-1 && j>=0) (f[i][j]+=f[i-1][j-1]*(k-(j-1))%mod)%=mod;
			}
		}
		for(int j=1;j<=n;j++) (ans+=f[m][j])%=mod;
	}
	printf("%lld\n",ans);
	return 0;
}

109.[POI2015] MYJ

一个店的价格肯定只会取某个 \(c_i\),所以可以离散化。
因为顾客是一段区间,所以考虑区间 DP。
\(f_{l,r,k}\) 表示当区间 \([l,r]\) 的最小值 \(\ge k\) 时,所有被 \([l,r]\) 包含的顾客的最大花费。
转移有:\(f_{l,r,k} = \max( f_{l,r,k+1} , \max_{x=l}^r (f_{l,x-1,k}+f_{x+1,r,k}+cnt(l,r,x,k)\times k) )\)
\(cnt(l,r,x,k)\) 表示当区间 \([l,r]\) 的最小值位置为 \(x\) 且值为 \(k\) 时,所有被 \([l,r]\) 包含的且在 \(x\) 处消费的顾客数。
直接转移是 \(O(n^3m^2)\),状态数是 \(O(n^2m)\),枚举 \(x\) \(O(n)\),求 \(cnt\) \(O(m)\)

只需要在枚举完 \(l,r\) 后去 \(O(nm)\) 预处理 \(cnt(l,r,x,k)\) 就可以在转移时去掉 \(O(m)\)
具体的预处理方法就是先 \(O(m)\) 的去枚举所有被 \([l,r]\) 包含的顾客 \(i\),然后 \(O(n)\) 遍历 \(x\in [a_i,b_i]\),将 \(cnt(l,r,x,c_i)\) 加上 \(1\)
最后对 \(cnt(l,r,x)\) 做一遍后缀和即可。
复杂度变成 \(O(n^3m)\)

记录方案是朴素的。

code

#include<bits/stdc++.h>
#define PII	pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=4000+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,a[N],b[N],c[N],f[55][55][N],dis[N],tot;
int cnt[55][N];
PII from[55][55][N];  //记录最小值的位置以及最小值 
int Dis(int x){
	return lower_bound(dis+1,dis+tot+1,x)-dis;
}
int p[N];
void dfs(int l,int r,int k){
	if(l>r) return;
	int x=from[l][r][k].fi,ming=from[l][r][k].se;
	if(ming==0) ming=k;
	p[x]=dis[ming];
	if(l==r) return;
	dfs(l,x-1,ming),dfs(x+1,r,ming);
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	n=read(),m=read();
	for(int i=1;i<=m;i++) a[i]=read(),b[i]=read(),dis[i]=c[i]=read();
	sort(dis+1,dis+m+1);
	tot=unique(dis+1,dis+m+1)-dis-1;
	for(int i=1;i<=m;i++) c[i]=Dis(c[i]);
	
	for(int len=1;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			
			//预处理 cnt 
			for(int x=l;x<=r;x++) for(int k=1;k<=tot;k++) cnt[x][k]=0;
			for(int i=1;i<=m;i++)
				if(l<=a[i]&&b[i]<=r)
					for(int x=a[i];x<=b[i];x++) cnt[x][c[i]]++;
			for(int x=l;x<=r;x++) for(int k=tot-1;k>=1;k--) cnt[x][k]+=cnt[x][k+1];
			
			for(int k=tot;k>=1;k--){
				from[l][r][k]=from[l][r][k+1],f[l][r][k]=f[l][r][k+1];
				for(int x=l;x<=r;x++){
					if(f[l][x-1][k]+f[x+1][r][k]+cnt[x][k]*dis[k] >= f[l][r][k]){
						f[l][r][k]=f[l][x-1][k]+f[x+1][r][k]+cnt[x][k]*dis[k];
						from[l][r][k]={x,k};
					}
				}				
			} 
		}
	}
	
	printf("%d\n",f[1][n][1]);
	dfs(1,n,1);
	for(int i=1;i<=n;i++) printf("%d ",p[i]);
	puts("");
	return 0;
}

110.[清华集训 2016] 组合数问题

这个一眼 Lucas 定理,\(C(n,m) \equiv 0 \pmod k\) 当且仅当 \(n,m\)\(k\) 进制表示下,\(n\) 有一位比 \(m\) 小。
因为 \(n,m\) 巨大,所以只能数位 DP:
\(f_{pos,0/1,0/1,0/1,0/1}\) 表示考虑了前 \(pos\) 位,目前 \(i\) 有没有一位比 \(j\) 小,\(i\)\(j\) 是否紧贴,\(i\)\(n\) 是否紧贴,\(j\)\(m\) 是否紧贴的方案数。
转移比较显然。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,k,n,m,len1,len2,a[65],b[65],f[65][2][2][2][2]; 
int dfs(int pos,bool flag,bool lim1,bool lim2,bool lim3){
	if(!pos) return flag;
	if(~f[pos][flag][lim1][lim2][lim3]) return f[pos][flag][lim1][lim2][lim3];
	int p1=lim2?a[pos]:(k-1),res=0;
	for(int i=0;i<=p1;i++){
		int p2=lim3?b[pos]:(k-1);
		if(lim1) p2=min(p2,i);
		for(int j=0;j<=p2;j++){
			( res += dfs(pos-1 , flag|(i<j) , lim1&(j==i) , lim2&(i==a[pos]) , lim3&(j==b[pos])) ) %= mod;
		}
	}
	return f[pos][flag][lim1][lim2][lim3]=res;
}
int solve(){
	len1=len2=0;
	memset(a,0,sizeof a); 
	memset(b,0,sizeof b); 
	while(n) a[++len1]=n%k,n/=k;
	while(m) b[++len2]=m%k,m/=k;
	memset(f,-1,sizeof f);
	return dfs(max(len1,len2) , false , true , true , true);
}

int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1;
	}
	return ans;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	T=read(),k=read();
	while(T--){
		n=read(),m=read();
		if(k==1){
			if(n>=m) printf("%lld\n",((m+1)*m%mod*quick_power(2,mod-2)%mod + (m+1)*(n-m+1)%mod)%mod);
			else printf("%lld\n",(n+2)*(n+1)%mod*quick_power(2,mod-2)%mod);
		}
		else printf("%lld\n",solve());
	}
	return 0;
}

111.Rikka with Subsequences

题意:
给一个 01 矩阵 \(A\),和一个数组 \(a\),定义 \(a\) 的长度为 \(m\) 的子序列 \(p\) 是好的,当且仅当:\(A_{p_1,p_2}=A_{p_2,p_3}=...=A_{p_{m-1},p_m}=1\)
特殊的,长度为 1 的子序列也是好的。
对于每一个本质不同的好的子序列 \(p\),若他在 \(a\) 中出现了 \(cnt\) 次,则答案加上 \(cnt^3\)
求最终答案。

当要求 \(cnt^3\) 时一个经典的套路是:\(cnt^3 = (1+1+...+1)(1+1+...+1)(1+1+...+1)\)
根据乘法分配律,这个等价于在每个括号里选一个 \(1\) 的方案数。
相当于是把原序列 \(a\) 复制成三个一模一样的序列 \(a\)\(b\)\(c\),求从 \(a\)\(b\)\(c\) 中选出本质相同好的子序列的方案数。
那么设 \(f_{i,j,k}\) 表示匹配到 \(a_i,b_j,c_k\) 的方案数 (要求 \(a_i=b_j=c_k\)),转移就是:
\(f_{i,j,k}=1+\sum_{i'<i,j'<j,k'<k,A_{a_i,a_{i'}}=1,a_{i'}=b_{j'}=c_{k'}} f_{i',j',k'}\)
直接做 \(O(n^6)\),可以前缀和优化+容斥变成 \(O(n^3)\)

这个前缀和优化比较高级,具体可以看代码。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e2+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,A[N][N],a[N],f[N][N][N],pre[N][N][N];
/*
	pre[i][j][k]:表示 Σf[i'][j'][k'] 且 i'<i,j'<j,k'<k 并且 A[ a[i'] ][ a[j] ] = 1。
	千万注意最后不是  A[ a[i'] ][ a[i] ] = 1
*/
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>T;
	while(T--){
		cin>>n;
		for(int i=1;i<=n;i++) cin>>a[i];
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				char c; cin>>c;
				A[i][j]=c-'0';
			}
		}	
		memset(f,0,sizeof f);
		memset(pre,0,sizeof pre);
		int ans=0;
		for(int i=1;i<=n;i++){
			
			//1.上一层的 f 没有用了,直接在原数组上对后两维做一遍前缀和(不带限制),注意容斥 
			for(int j=1;j<=n;j++)
				for(int k=1;k<=n;k++)
					(f[i-1][j][k]+=((f[i-1][j-1][k]+f[i-1][j][k-1])%mod-f[i-1][j-1][k-1]+mod)%mod)%=mod;  
			
			//2.计算当前的 pre[i][j][k]:pre[i][j][k] 和 pre[i-1][j][k] 相比由于我们的定义,其实只多了 Σf[i-1][j'][k'] (如果 A[a[i-1]][a[j]]=1 的话)
			for(int j=1;j<=n;j++)
				for(int k=1;k<=n;k++)
					if(A[a[i-1]][a[j]]) pre[i][j][k]=(pre[i-1][j][k]+f[i-1][j-1][k-1])%mod;  //这个时候的 f 数组已经做过前缀和了 
					else pre[i][j][k]=pre[i-1][j][k];
			
			//3.计算当前层的 f 
			for(int j=1;j<=n;j++)
				for(int k=1;k<=n;k++){
					if(a[i]==a[j]&&a[j]==a[k]) f[i][j][k]=(pre[i][j][k]+1)%mod;   //虽然 pre 的定义是 A[ai'][aj]=1,但是这里 ai=aj 所以没事
					else f[i][j][k]=0;	
					(ans+=f[i][j][k])%=mod;
				}
		}
		cout<<ans<<'\n';
	}
	return 0;
}

112.Bus Analysis

考虑给你了一个序列 \(t\) 怎么算答案,然后 dp of dp 即可。
首先把贡献都除以二,最后再把答案 \(\times 2\) 没有任何影响。

朴素的 dp 是设 \(f_i\) 表示覆盖前 \(i\) 个点的最小代价。
转移时,如果区间 \([t_{j+1},t_i]\) 的长度 \(\le 20\) 则有转移 \(f_j+1 \to f_i\);如果长度 \(\le 75\) 则有转移 \(f_j+3 \to f_i\)
这么看的话,如果想要转移 \(f_i\) 我们需要至多 \(i\) 前面 \(75\)\(j\) 的 DP 值。

继续压缩,会发现这 \(75\)\(j\) 中的任意两个 \(x,y\) 都满足 \(f_x\)\(f_y\) 相差不超过 \(3\)
所以其实有用的 DP 值只有三种。
于是我们内层 dp 其实只需要维护四个值:

  1. \(w\):表示 \(f_i\)
  2. \(x\): 表示最大的 \(j\) 满足 \(f_j=f_i-1\)
  3. \(y\): 表示最大的 \(j\) 满足 \(f_j=f_i-2\)
  4. \(z\): 表示最大的 \(j\) 满足 \(f_j=f_i-3\)

:理论上讲我们还需要维护 \(u\) 表示最大的 \(j\) 满足 \(f_j=f_i\),但显然 \(u=i\)
转移是简单的,先算出 \(f_{i+1}\),然后用 \({i,x,y,z}\) 去得到新的 \((x',y',z')\)

那么我们的外层 dp 就是 \(f_{i,w,x,y,z}\) 表示内层 dp 的结果是 \((w,x,y,z)\) 的方案数,显然会炸。
不过发现我们记录 \(w\) 的唯一用处就是计算最后的答案。
但是我们只需要计算所有 \(w\) 的和即可,这启发我们在 \(w\) 改变的时候直接把变化值加到最后的答案里就可以了。
这样状态就变成 \(f_{i,x,y,z}\) 了,\(O(n\times 75^3)\) 因为常数小可以通过。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5,mod=1e9+7;

inline int read(){
	int w=1,s=0;
	char c=getchar();
	for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
	for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
	return w*s;
}
int n,t[N],f[2][76][76][76],pos[10],ans,p[N];
void DP1(int i,int x,int y,int z,int res){
	if(x) (x+=1)%=76;
	if(y) (y+=1)%=76;
	if(z) (z+=1)%=76;
	(f[i&1^1][x][y][z]+=res)%=mod;
}
int solve(int now,int lst,int res){
	int len=t[now]-t[lst+1]+1;
	if(len<=20) return res+1;
	else if(len<=75) return res+3;
	else return n+5;
}
void DP2(int i,int x,int y,int z,int res){
	int g=solve(i+1,i,4);
	if(x) g=min(g,solve(i+1,i-x,3));
	if(y) g=min(g,solve(i+1,i-y,2));
	if(z) g=min(g,solve(i+1,i-z,1));
	(ans+=1ll*res*p[n-i-1]%mod*(g-4)%mod)%=mod;
	pos[1]=pos[2]=pos[3]=0;
	if(z>=1&&z<=74) pos[g-1]=z+1;
	if(y>=1&&y<=74) pos[g-2]=y+1;
	if(x>=1&&x<=74) pos[g-3]=x+1;
	pos[g-4]=1;
	(f[i&1^1][pos[1]][pos[2]][pos[3]]+=res)%=mod;
}
signed main(){
	n=read();
	p[0]=1;
	for(int i=1;i<=n;i++) t[i]=read(),p[i]=p[i-1]*2%mod;
	
	f[0][0][0][0]=1;
	for(int i=0;i<n;i++){
		memset(f[i&1^1],0,sizeof f[i&1^1]);
		for(int x=0;x<=min(i,75);x++){
			for(int y=0;y<=min(i,75);y++){
				for(int z=0;z<=min(i,75);z++){
					if(!f[i&1][x][y][z]) continue;
					DP1(i,x,y,z,f[i&1][x][y][z]);
					DP2(i,x,y,z,f[i&1][x][y][z]);
				}
			}
		}
	}
	
	printf("%d\n",ans*2%mod);
	return 0;
}

113.[NOIP2021] 方差

先模一下第一篇题解的数学大师。

首先虽然很难发现但是不难证明操作相当于是交换差分数组。
而题目要求的式子可以写成 \(n\times \sum a_i^2 - (\sum a_i)^2\)

考虑运用我们初二的数学知识,方差越小意味着数据波动越小。
也就是我们在把序列排序后,他的图像应该是:增长速度先变缓,然后保持在平均数,再快速增长。
在差分数组上的表示就是差分数组先减小后增大,呈单谷。
所以我们先把差分数组升序排序。
那么不难想到按差分的值从小到大 dp,每一次往差分数组里面放数,要么放开头要么放结尾。
但是这样不太方便维护上面那个式子,于是考虑把一个值放进 dp。
\(f_{i,j}\) 表示放了 \(i\) 个差分值,且目前还原出的原序列的 \(\sum a_i\)\(j\)\(\sum a_i^2\) 的最小值。
转移时:

  1. 下一个放开头:\(f_{i,j} + 2\times j\times c_{i+1} + (i+1)\times (c_{i+1}^2) \to f_{i+1,j+(i+1)\times c_{i+1}}\)
  2. 下一个放结尾:\(f_{i,j} + pre_{i+1}^2 \to f_{i+1,j+pre_{i+1}}\)\(pre_{i+1}=\sum_{j=1}^{i+1} c_j\)
    目前时间复杂度是 \(O(n^2V)\) 的。

因为原数组有序,而我们会发现 \(n\) 远大于 \(V\) 所以有很多差分为 \(0\),这一部分没有必要转移。
当然不转移不代表不给他状态,你不能直接去掉那些为 \(0\) 的差分值(可能只有我一开始是这么写的)。
差分不为 \(0\) 的只有 \(V\) 个,所以复杂度变为 \(O(nV^2)\)
然后因为这个数据 \(n\)\(a\) 成反比,所以不想把代码复制一份的话建议滚动数组。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5,inf=0x3f3f3f3f3f3f3f3f;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,a[10005],t[10005],c[10005],pre[10005],sum,f[2][N]; 
signed main(){
	n=read();
	int maxn=0;
	for(int i=1;i<=n;i++) a[i]=read(),maxn=max(maxn,a[i]);
	sum=n*maxn;
	for(int i=1;i<n;i++) c[i]=a[i+1]-a[i]; 
	
	sort(c+1,c+n);
	
	m=n-1;
	for(int i=1;i<=m;i++) pre[i]=pre[i-1]+c[i];
	
	memset(f,0x3f,sizeof f);
	f[0][0]=0;
	for(int i=0;i<m;i++){
		if(c[i+1]==0) f[i&1^1][0]=0;
		else{
			for(int j=0;j<=sum;j++) f[i&1^1][j]=inf;
			for(int j=0;j<=sum;j++){
				if(j+(i+1)*c[i+1]<=sum) f[(i+1)&1][j+(i+1)*c[i+1]] = min(f[(i+1)&1][j+(i+1)*c[i+1]] , f[i&1][j] + 2*j*c[i+1] + (i+1)*(c[i+1]*c[i+1]));
				if(j+pre[i+1]<=sum) f[(i+1)&1][j+pre[i+1]] = min(f[(i+1)&1][j+pre[i+1]] , f[i&1][j] + pre[i+1]*pre[i+1]);
			}
		}
	}
	int ans=inf;
	for(int i=0;i<=sum;i++){
		if(f[m&1][i]==inf) continue; 
		ans=min(ans,n*(f[m&1][i] + 2*i*a[1] + n*a[1]*a[1]) - (i+n*a[1]) * (i+n*a[1]));
	}
	printf("%lld\n",ans);
	return 0;
}

114.Match

题目翻译:
有两个序列 \(a,b\),以及一张二分图,二分图中左部节点 \(i\) 和右部节点 \(j\) 有边的条件是:\(a_i\oplus b_j \ge k\)
对每个 \(1\le k\le n\) 求二分图大小为 \(k\) 的匹配的数量。

这种异或起来大于或小于某个数的题以前有做过:CF1616H。

两个思路:一个在 Trie 上考虑,一个直接在序列上考虑,两者殊途同归,为了方便理解我们在 Trie 上考虑。

因为题目要求的是匹配数量,数据范围又很小,考虑直接 dp。
注意因为有两个序列所以我们维护两棵 Trie。
\(f(u,v,i)\) 表示在 \(a\) 序列的 Trie 的 \(u\) 子树和 \(b\) 序列的 Trie 的 \(v\) 子树内选出大小为 \(i\) 的匹配的方案数。

  1. 如果 \(k\) 当前这一位为 \(1\)
    那么一对匹配不能同时在 \(ch1_{u,0},ch2_{v,0}\) 或同时在 \(ch1_{u,1},ch2_{v,1}\) 子树内。
    \(f(ch1_{u,0},ch2_{v,1},x)\times f(ch1_{u,1},ch2_{v,0},y) \to f(u,v,x+y)\)
  2. 如果 \(k\) 当前这一位为 \(0\),此时的匹配可以是来自:
  • \(f(ch1_{u,0},ch2_{v,0},x)\)
  • \(f(ch1_{u,1},ch2_{v,1},y)\)
  • \(ch1_{u,0}\)\(ch2_{v,1}\) 随便组合。
  • \(ch1_{u,1}\)\(ch2_{v,0}\) 随便组合。
    可先算出后两个组合的方案数,再算出四个合在一起的方案数。

代码实现时注意到你开不下 \(O(60^2n^3)\) 的数组,但是儿子的 dp 数组只在父亲有用到,所以在 dfs 时直接返回 dp 数组,也不需要真的去把 Trie 树建出来。
复杂度的话,因为这个转移本质是树形背包,所以其实是 \(O(n^4)\)

代码参考了一篇题解。
因为 vector 都是动态开空间,所以一定要注意每个 vector 到底要开多少。

这应该是我写过的最高级的代码(指语法)。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e2+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,inv[N],fact[N],q[N];
int C(int n,int m){
	if(n<m) return 0;
	return fact[n]*q[m]%mod*q[n-m]%mod;
}
void Init(){
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	q[0]=1;
	for(int i=1;i<N;i++) q[i]=q[i-1]*inv[i]%mod;
}

vector<int> merge1(vector<int> f1,vector<int> f2){  //合并两个 dp 数组 
	vector<int> dp(f1.size()+f2.size()-1,0);
	for(int i=0;i<f1.size();i++)
		for(int j=0;j<f2.size();j++)
			(dp[i+j]+=f1[i]*f2[j]%mod)%=mod;
	return dp;
}

vector<int> merge2(int n,int m){  //表示 n 个左部节点和 m 个右部节点随意匹配的方案数 
	vector<int> res(min(n,m)+1);
	for(int i=0;i<res.size();i++) res[i]=C(n,i)*C(m,i)%mod*fact[i]%mod;
	return res;
}

vector<int> dfs(int pos,vector<int> &a,vector<int> &b){
	
	if(a.empty() || b.empty()) return {1};
	if(pos==-1) return merge2(a.size(),b.size());
	
	vector<int> A0,A1,B0,B1;
	for(int x:a) if(x>>pos&1) A1.emplace_back(x); else A0.emplace_back(x);
	for(int x:b) if(x>>pos&1) B1.emplace_back(x); else B0.emplace_back(x);
	
	if(k>>pos&1) return merge1(dfs(pos-1,A0,B1),dfs(pos-1,A1,B0));
	
	vector<int> f1=dfs(pos-1,A0,B0),f2=dfs(pos-1,A1,B1);
	vector<int> dp(min(a.size(),b.size())+1,0);
	for(int i=0;i<f1.size();i++){
		for(int j=0;j<f2.size();j++){
			int lstA0=A0.size()-i,lstA1=A1.size()-j;   //计算剩余可供合并的点 
			int lstB0=B0.size()-i,lstB1=B1.size()-j;
			vector<int> f3=merge1(merge2(lstA0,lstB1) , merge2(lstA1,lstB0));  //先计算随便匹配的方案数。 
			for(int k=0;k<f3.size();k++) (dp[i+j+k]+=f1[i]*f2[j]%mod*f3[k]%mod)%=mod;
			f3.clear(); f3.shrink_to_fit();  //释放空间 
		}
	}
	  
	f1.clear(); f1.shrink_to_fit();  //释放空间 
	f2.clear(); f2.shrink_to_fit();  //释放空间
	
	return dp;
}
signed main(){
	n=read(),k=read();
	vector<int> a(n),b(n);
	for(int i=0;i<n;i++) a[i]=read();
	for(int i=0;i<n;i++) b[i]=read();
	
	Init();
	
	vector<int> ans=dfs(60,a,b);
	while(ans.size()<n+1) ans.emplace_back(0);
	for(int i=1;i<=n;i++) printf("%lld\n",ans[i]);
	return 0;
}

115.[AGC009E] Eternal Average

考虑把最终的树画出来:
这棵 \(k\) 叉树有 \(n+m\) 个叶子,\(n\) 个叶子权值为 \(0\)\(m\) 个叶子的权值为 \(1\),父亲的权值为儿子权值的平均数,最终剩的数就是根的权值。
因为相当于是每一次少 \(k-1\) 个点,所以题目保证了 \((k-1)|(n+m-1)\)

\(m\) 个点的深度分别为 \(x_i\)(根的深度为 \(0\)),\(n\) 个点的深度分别为 \(y_i\),那么容易知道根的权值为 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}\)
而如果所有叶节点的权值均为 \(1\),那么根的权值为 \(1\),所以 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}+\sum_{i=1}^n (\frac{1}{k})^{y_i} = 1\)
而如果给定了 \(x_i\)\(y_i\) 且满足 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}+\sum_{i=1}^n (\frac{1}{k})^{y_i} = 1\),那肯定也可以构造出一棵合法的 \(k\) 叉树。

证明:
考虑最后的 \(1\) 怎么得到的,首先深度为 \(1\) 的层必然有 \(k\) 个点。
然后我们把满足 \(x_i=1,y_j=1\) 的点分配到第 \(1\) 层,那么第 \(1\) 层还会剩下一些点,这些点是由第 \(2\) 层合并上来的。
由此不断往下一层考虑,每一层的点必然都是 \(k\) 的倍数,而且 \(n+m\) 个点都能得到分配。
当然你也可以把这个过程看成在 \(k\) 进制下的小数的加法的逆过程(即不断退位)。

现在问题变成:求满足 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}+\sum_{i=1}^n (\frac{1}{k})^{y_i} = 1\) 的不同 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}\) 的个数。

可以发现,在 \(k\) 进制下,如果不考虑进位,\(\sum_{i=1}^m (\frac{1}{k})^{x_i}\) 的数位和 \(= m\)(我们下面记它为 \(0.c_1c_2c_3....c_l\)),即 \(\sum_{j=1}^l c_j= m\)
因为此时数位和的实际意义就是有多少个值为 \(1\) 的叶子。
那么对于一个进位过的 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}\),由于进位的过程是把 \(j+1\)\(-k\),第 \(j\)\(+1\),所以此时的 \(\sum_{j=1}^l c_j \equiv m \pmod{k-1}\)
而如果一个小数 \(0.c_1c_2c_3....c_l\) 他的数位和 \(sum\le m\)\(sum \equiv m \pmod{k-1}\),那么我们通过不断退位总可以使 \(sum=m\),此时就可以构造出合法的 \(x\) 序列。
而因为 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}+\sum_{i=1}^n (\frac{1}{k})^{y_i} = 1\),所以 \(\sum_{i=1}^n (\frac{1}{k})^{y_i} = 1-\sum_{i=1}^m (\frac{1}{k})^{x_i}\),所以 \(\sum_{i=1}^n (\frac{1}{k})^{y_i}\) 的数位和为 \(1+l\times (k-1)-sum\)
类似的可以证明它同样需要与 \(n\)\(k-1\) 同余。
于是我们得出了一个结果 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}\) 合法的充要条件(\(sum\) 是数位和,\(l\) 是位数):

  1. \(sum \equiv m \pmod{k-1}\)
  2. \(1+l\times (k-1)-sum \equiv n \pmod{k-1}\)

然后就可以 DP 了。
\(f_{i,j,0/1}\) 表示考虑了前 \(i\) 个小数位,位数和为 \(j\),最后一位是否是 \(0\) 能构成的不同 \(\sum_{i=1}^m (\frac{1}{k})^{x_i}\) 的方案数。
转移就是个朴素的背包。
当且仅当 \(j \equiv m \pmod{k-1}\)\(1+i\times (k-1)-j \equiv n \pmod{k-1}\) 且末尾没有后导 \(0\) 时,可以统计入答案。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,k,f[N<<1][N][2],pre[N<<1][N],ans=0; 
signed main(){
	n=read(),m=read(),k=read();
	f[0][0][0]=1;
	for(int j=0;j<=m;j++) pre[0][j]=1;
	for(int i=1;i<=n+m-1;i++){
		for(int j=0;j<=m;j++){
			f[i][j][0]=(f[i-1][j][0]+f[i-1][j][1])%mod;
			int tmp=min(j,k-1);
			if(tmp>=1) f[i][j][1] = ( pre[i-1][j-1] - ((j-tmp-1>=0)?pre[i-1][j-tmp-1]:0) + mod ) % mod;
			pre[i][j] = (((j==0) ? 0 : pre[i][j-1]) + f[i][j][0] + f[i][j][1] ) % mod;
			
			if((m-j)%(k-1)==0 && (1+i*(k-1)-j<=n) && (n-1-i*(k-1)+j)%(k-1)==0) (ans+=f[i][j][1])%=mod;
		}
	}
	printf("%lld\n",ans);
	return 0;
}

116.[NOI2008] 奥运物流

因为题目说每个点都可以到 \(1\) 号点,所以 \(1\) 一定在那个环中。

首先先考虑如果不是基环树,而是一棵以 \(1\) 为根的内向树时该怎么做。
此时容易得到 \(R(1) = \sum_{i=1}^n C_i\times k^{dep_i}\),其中 \(dep_1=0\)
那么我们每一次操作一定是把一个点的后继点直接改成 \(1\),即把它树上的父亲变为 \(1\) 号点。
因为最后的答案只跟 \(dep\) 有关,所以我们只需要知道每个点在最终树上的深度即可,考虑树形 DP。
\(f_{i,j,k}\) 表示:当前在考虑 \(i\) 子树,且 \(i\) 子树内(不包含 \(i\))有 \(j\) 个点的父亲被修改了,\(i\) 在最终得到的树的深度为 \(k\) 时,\(i\) 子树内(包含 \(i\))的点的 \(\sum C_i\times k^{dep_i}\) 的最大值。
转移时考虑树形背包,每次加进来 \(u\) 的一个子树 \(v\)

  • \(v\) 自己不修改,那 \(v\) 的深度就是 \(u\) 的深度 \(+1\)\(f_{u,i,k}+f_{v,j,k+1} \to f_{u,i+j,k}\)
  • \(v\) 自己修改了,那 \(v\) 的深度为 \(1\)\(f_{u,i,k}+f_{v,j,1} \to f_{u,i+j+1,k}\)

因为是树形背包,所以单次树形 DP 是 \(O(n^3)\)

考虑基环树,对于环上的点列 \(len\) 元一次方程组(\(len\) 为环长),解一下可以得到 \(R(1)=\dfrac{\sum_{i=1}^n C_i\times k^{dist_i}}{1-k^{len}}\),其中 \(dist(i)\) 表示 \(i\) 需要走几步可以到 \(1\)(其实上面的 \(dep\) 也可以写成 \(dist\))。
假设环是 \(1\to x_1\to x_2\to ... \to x_{len-1}\)
如果环上有点需要修改,那也一定是连向 \(1\),因为这样分母变小,分子变大,整体变大。
我们枚举第一个修改的点 \(x_i\),让他指向 \(1\),现在环变成了 \(1\to x_1\to x_2\to ... \to x_i\),环长变为 \(i+1\)
然后会发现最后的式子只跟 \(dist\) 和环长有关,现在环长已经确定为 \(i\) 了,而在算 \(dist\) 时一定不会用到 \(1 \to x_1\) 这条边。
所以我们可以现在直接把这条边断掉,这样的话图就变成了一棵树,可以直接跑树形 DP。
总时间复杂度 \(O(n^4)\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=60+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,nxt[N];
double k,p[N],C[N];

int st[N],top;
vector<int> G[N];
PII novis;
double f[N][N][N],ans;
int Size[N];
void dfs(int u,int dep){
	Size[u]=1;
	for(int i=0;i<=dep;i++) f[u][0][i]=p[i]*C[u];
	for(int v:G[u]){
		if(u==novis.fi && v==novis.se) continue;
		dfs(v,dep+1);
		for(int x=Size[u];x>=0;x--){
			for(int y=Size[v];y>=0;y--){
				if(x+y>m) continue;
				for(int i=0;i<=n;i++){
					if(x+y+1<=m) f[u][x+y+1][i]=max(f[u][x+y+1][i],f[u][x][i]+f[v][y][1]);
					f[u][x+y][i]=max(f[u][x+y][i],f[u][x][i]+f[v][y][i+1]);
				}
			}
		}
		Size[u]+=Size[v]; 
	}
} 
signed main(){
	scanf("%d%d%lf",&n,&m,&k);
	for(int i=1;i<=n;i++) scanf("%d",&nxt[i]);
	for(int i=1;i<=n;i++) scanf("%lf",&C[i]);
	p[0]=1.0;
	for(int i=1;i<=n;i++) p[i]=p[i-1]*k;
	
	int u=1;
	st[++top]=1;
	while(nxt[u]!=1) st[++top]=nxt[u],u=nxt[u];
	for(int i=2;i<=n;i++) G[nxt[i]].push_back(i);  //自动忽略 (nxt[1],1) 这条边 
	for(int i=2;i<=top;i++){
		if(i!=top && m==0) continue;
		if(i!=top){
			novis={st[i+1],st[i]};
			m--;  //已经用掉了一次 
			G[1].push_back(st[i]);
		}
		for(int i=1;i<=n;i++) for(int j=0;j<=m;j++) for(int d=0;d<=n;d++) f[i][j][d]=-1e9;
		dfs(1,0);
		ans=max(ans,f[1][m][0]/(1.0-p[i]));  //因为这里的 i 算上了 1 号点,所以不用写 i+1 
		if(i!=top){
			novis={0,0};
			m++;
			G[1].pop_back(); 
		}
	}
	printf("%.2lf\n",ans);
	return 0;
}

117.[THUWC 2017] 随机二分图

首先根据期望的定义,最后的期望等于每个完美匹配出现的概率之和。
而我们在算每个完美匹配出现的概率时,只需要算上完美匹配中的边出现的概率即可,其他的边出不出现都无所谓,不用算进概率。
也就是说如果一组里的边并没有出现在匹配里,这组边的概率其实不用管。

考虑只有第一类组怎么做。
\(f_{i,S}\) 表示左部节点的前 \(i\) 个点匹配了右部 \(S\) 中的点的匹配的期望个数。
转移时考虑所有以 \(i+1\) 为左部节点的边即可。

现在我们加上第二,三两类组,因为此时我们加点的顺序会是无序的(即左部节点可能不是从 1 顺序加到 n 的),所以朴素的思想是设 \(f_{S,T}\) 表示左部 \(S\) 的点匹配了右部 \(T\) 的点的匹配的个数。
但这会有一个问题,就是第二组里的两条边虽然会同时出现在图中,但不一定同时出现在匹配中,换句话说我们需要钦定第二组里的边到底是一下哪种状态:

  1. 只有第一条边在匹配中。
  2. 只有第二条边在匹配中。
  3. 两条边全在匹配中。
  4. 两条边全不在匹配中(当然这个情况并不会对概率做出贡献,其实不用考虑)。

但是注意到在转移 \(f_{S,T}\) 时我们记录不了哪些组被转移过了,我们其实只能通过这组里的边的点是否在 \(S\)\(T\) 里出现了来判断。
如果只有第一类组,这种转移没有任何的问题,但是当有了第二类组就会有问题,因为下面两种情况本质是一样的,但都会被算一次,就重复了:

  • 第一次就钦定两条边全出现在匹配里。
  • 第一次只钦定出现了第一条边,第二次再考虑到这条边时又钦定了只出现第二条边。

而我们并不能去记录每个第二类组的状态,所以不能这么转移。

然后就有一个非常神奇的思路:既然我们只能处理第一类组,那不妨把第二三类组都拆成两个第一类组,然后看一下这么转移会多转移或少转移什么东西。

对于第二类组,假设它里面的两条边是 \((u,v)\)\((x,y)\),现在它已经变成两个分别包含了 \((u,v)\)\((x,y)\) 的第一类组。

  • 当最后只有 \((u,v)\) 在完美匹配中,即对应了状态 2.,对概率的贡献是 \(\frac{1}{2}\)
  • 当最后只有 \((x,y)\) 在完美匹配中,即对应了状态 3,对概率的贡献是 \(\frac{1}{2}\)
  • 当最后 \((x,y)\)\((u,v)\) 全在完美匹配中,即对应了状态 4.,那此时的贡献是 \(\frac{1}{4}\),可其实概率是 \(\frac{1}{2}\)
    那我们只需要多加一个表示 \((x,y)\)\((u,v)\) 全在完美匹配中的,且系数为 \(\frac{1}{4}\) 的转移就可以把上面漏掉的那 \(\frac{1}{4}\) 的概率加上。

对于第三类组,同样假设它里面的两条边是 \((u,v)\)\((x,y)\),现在它已经变成两个分别包含了 \((u,v)\)\((x,y)\) 的第一类组。

  • 前面两个情况的分析是一样的,没有问题。
  • 当最后 \((x,y)\)\((u,v)\) 全在完美匹配中,那此时的贡献是 \(\frac{1}{4}\),可其实概率是 \(0\),因为他们压根不能同时出现。
    那我们只需要多加一个表示 \((x,y)\)\((u,v)\) 全在完美匹配中的,且系数为 \(-\frac{1}{4}\) 的转移就可以把上面多的的那 \(\frac{1}{4}\) 的概率减掉。

然后为了防止把不同的加入顺序算成不同方案,每一次可以钦定只转移编号最小的左部节点。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,inv2,inv4;
int a[N],b[N],cnt;  //分别表示转移和这个转移的系数 
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		b>>=1,(a*=a)%=mod; 
	}
	return ans;
}
map<int,int> f;
int dfs(int S){
	if(f.count(S)) return f[S]; // 别写 f[S]!=0 因为 f[S] 可以 =0 
	for(int i=0;i<n;i++){
		if(!(S>>i&1)){  //不用考虑是否有组包含编号最小的没被算进匹配的点,因为没有的话这个图一定没有完美匹配,答案为0 
			int res=0;
			for(int j=1;j<=cnt;j++){
				if( (a[j]>>i&1)==1 && (a[j]&S)==0 ){  //当前转移得包含 i,并且没有在 S 中出现过 
					(res+=b[j]*dfs(S|a[j])%mod)%=mod; 
				}
			}
			return f[S]=res;
		}
	}
}
signed main(){
	inv2=quick_power(2,mod-2),inv4=quick_power(4,mod-2);
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int op=read(),x=read()-1,y=read()-1;
		a[++cnt]=(1<<x)|(1<<(y+n));
		b[cnt]=inv2;
		if(op>0){
			int u=read()-1,v=read()-1;
			a[++cnt]=(1<<u)|(1<<(v+n));  //直接压成一个二进制数,很高级 
			b[cnt]=inv2;
			if(x==u || y==v) continue;  //这种情况绝对不可能出现 (x,y) 和 (u,v) 一起出现在匹配中的情况
			int tmp=(1<<x)|(1<<(y+n)) | (1<<u)|(1<<(v+n));
			if(op==1) a[++cnt]=tmp,b[cnt]=inv4;
			else a[++cnt]=tmp,b[cnt]=-inv4+mod;
		}
	}
	f[(1<<(n<<1))-1]=1;
	printf("%lld\n",quick_power(2,n)*dfs(0)%mod); 
	return 0;
}

118.[ARC098F] Donation

考虑正难则反:
即假设我们现在在终点,往回退着走,那么假设在第一次到达点 \(u\) 时我有 \(x\) 元钱,那需要满足 \(x\ge \max(0,A_u-B_u)\),然后在这个点时会得到 \(B_u\) 的钱。
就算我们后面可能会再回到这个点,但是注意到倒着做的话我们的钱只增不减,而现在 \(\ge A_u-B_u\),那加完 \(B_u\) 后钱数就 \(\ge A_u\) 了,所以后面就算回到 \(u\) 也不用担心钱数会 \(<A_u\)
这样总答案就是在终点的钱数 \(+\sum_{i=1}^n B_i\)

但是你如果按照 \(C_u=\max(0,A_u-B_u)\) 从小到大贪心的遍历会出问题,因为你从 \(u\) 走到 \(v\) 的过程中可能会经过其他 \(C\) 更大的点。
考虑以 \(C\) 建出 Kruskal 重构树(边权设为两个端点的 \(C\) 的最大值),然后树形 DP,设 \(f_u\) 表示遍历完 \(u\) 子树所需要的最小钱数,\(sum_u\) 表示 \(u\) 子树内所有叶子的 \(B\) 的和。
那么枚举起点在哪棵子树内,假设在 \(v\) 子树内,那么遍历完 \(v\) 后你会有 \(x+sum_v\) 个钱(\(x\) 是初始的钱数),此时你需要满足 \(x+sum_v\ge C_u\)
然后因为 \(C_u\) 是其子树内所有 \(C\) 最大的,那么在到达 \(u\) 之后再往下去其他子树就一定满足了。
所以转移为: \(f_u=\min_v(\max(C_u-sum_v,f_v))\)\(C_u-sum_v\) 的意思是至少要这么多可以走到 \(u\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,tot,head[N],to[N],Next[N],A[N],B[N],C[N];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}
struct Edge{
	int u,v,w;
}e[N];
int fa[N],cnt;
int get(int x){
	return (x==fa[x])?x:(fa[x]=get(fa[x]));
} 

int sum[N],f[N];
void dfs(int u){
	if(!head[u]){
		sum[u]=B[u];
		f[u]=C[u];
		return;
	}
	f[u]=LLONG_MAX;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		dfs(v);
		sum[u]+=sum[v];
		f[u]=min(f[u],max(f[v],C[u]-sum[v]));
	}
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		A[i]=read(),B[i]=read();
		C[i]=max(A[i]-B[i],0ll);
	}
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=max(C[u],C[v]);
		e[i]={u,v,w};
	}
	
	for(int i=1;i<=2*n-1;i++) fa[i]=i;
	sort(e+1,e+m+1,[&](Edge x,Edge y){return x.w<y.w;});
	cnt=n;
	for(int i=1;i<=m;i++){
		int u=e[i].u,v=e[i].v,w=e[i].w;
		if(get(u)==get(v)) continue;
		u=get(u),v=get(v);
		++cnt;
		C[cnt]=w;
		add(cnt,u),add(cnt,v);
		fa[u]=cnt,fa[v]=cnt;
	}
	dfs(cnt);
	
	printf("%lld\n",f[cnt]+sum[cnt]);
	return 0;
}
posted @ 2025-04-03 19:29  Green&White  阅读(117)  评论(0)    收藏  举报