「动态规划 2022/02 」学习记录


Topcoder SRM 713 Medium DFS Count

给一个图,求不同的 DFS 序个数。

其中 \(n \le 18\) 。

设 \(f(S,i)\) 表示走过点集 \(S\) ,当前在 \(i\) 上。对于下一个点,必须知道之前可以回溯的点。

设 \(g(i,S)\) 表示从 \(i\) 出发经过 \(S\) 可以走到的点。

于是有转移,对于一个 \(j \in g(i,S)\) ,有 \(f(S \cup \{ j \},j) \leftarrow f(S \cup \{ j \},j) + f(S,i)\) 。

CF868E Policeman and a Tree

有一个 \(n\) 个点的树,有一个警察初始在 \(s\) 点,移动速度为 \(1\)。有 \(m\) 个小偷在 \(x_i\) 点,移动速度为无穷大。如果警察和小偷某个时刻在同一地点,小偷就会被抓住。

求最坏情况下警察抓住小偷需要的最少时间。

其中 \(n,m \le 50\) 。

首先本题一定有解。警察只有在把一个小偷怼到叶子节点的时候才会抓到。

考虑当前警察在 \(u\) 上,那么可以记录他周边点小偷的情况。但这个数量太大。

转为当警察在一条边 \((u,v)\) 上时的情况。设 \(f(e,x,y)\) 表示警察在 \(e\) 边上,\(v\) 子树内有 \(x\) 个小偷,子树外有 \(y\) 个。

如果 \(v\) 是叶子节点,则 \(f(e,x,y)=f(e',y,0)\) 。其中 \(e'\) 是 \(e\) 的反向边。

若不是叶子节点,那么在 \(v\) 上的小偷一定会跑到 \(v\) 连接的非 \(u\) 点。假设有 \(d\) 个。于是有:

\[f(e,x,y)=\max \limits_{c_1+c_2+...+c_d=x} \{ \min\limits_{s=1}^d\{ f(i,c_s,x+y-c_s) + w \} \} \]

于是考虑用类似背包的转移,设 \(g(i,j)\) 表示在 \(v\) 中的第 \(i\) 个子树放了 \(j\) 个罪犯的时间。

\[g(i,j)=\max\{ \min\{ g(i-1,j-k),f(e,k,x+y-k) +w \} \} \]

而 \(g\) 可以滚掉第一维,然后注意一下枚举时候的顺序,记忆化即可。复杂度 \(\mathcal{O}(n^5)\) 。

#include
#include
#include
#include
using namespace std;
const int N=50+5;
const int inf=1e9;
struct edge{
	int v,w,nx;
}e[N<<1];
int n,m,ne=1,st,ans,f[N],d[N],sz[N],dp[N<<1][N][N];
void read(int u,int v,int w)
{	e[++ne].v=v;
	e[ne].nx=f[u];
	e[ne].w=w;
	f[u]=ne;
	d[u]++;
}
void dfs(int u,int ffa)
{	for(int i=f[u];i;i=e[i].nx)
	{	int v=e[i].v;
		if(v==ffa)continue;
		dfs(v,u);
		sz[u]+=sz[v];
	}
}
int DP(int et,int x,int y)
{	if(!x&&!y)return 0;
	if(dp[et][x][y]!=-1)return dp[et][x][y];
	int u=e[et].v;
	if(d[u]==1)return (y==0)?(0):(DP(et^1,y,0)+e[et].w);
	int g[N];
	memset(g,0,sizeof(g));
	g[0]=inf;
	for(int i=f[u];i;i=e[i].nx)
	{	if(i==(et^1))continue;
		for(int j=x;j>=1;j--)
			for(int k=j;k>=1;k--)
				g[j]=max(g[j],min(g[j-k],DP(i,k,x+y-k)+e[i].w));
	}
	return dp[et][x][y]=g[x];
}
int main()
{	scanf("%d",&n);
	for(int i=1,u,v,w;i

CF762F Tree nesting 的题解。

给定两棵树 \(S,T\),求 \(S\) 有多少个连通子图与 \(T\) 同构。

其中 \(|S| \le 10^3,|T| \le 12\) 。

首先因为 \(T\) 很小,所以可以对每个点作根的情况状压。每次状压每个点连接的儿子即可。

那么对于 \(S\) 就不用枚举根,所以只要考虑每个子树即可。

记忆化搜索,每次搜索设 \(f(u,v,i)\) 表示在 \(u\) 子树内,匹配到第 \(v\) 个儿子的子树,匹配的状态是 \(i\) 的方案数。于是有:

\[f(u,v,i) \leftarrow f(u,v,i)+\sum\limits_{k \in i} f(v,sz_v,j)\times f(u,v-1,i \setminus \{ k \}) \]

其中 \(j\) 是点 \(k\) 的状压。

注意可能出现自己和自己配对的情况,所以最后的答案还要除以 \(T\) 和 \(T\) 同构匹配的情况。

#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N=1000+5,M=1<<12;
const int Mod=1e9+7;
struct edge{
	int v,nx;
};
struct tree{
	int n,ne,f[N],sz[N],bit[N],son[N][N];
	edge e[N<<1];
	void read(int u,int v)
	{	e[++ne].v=v;
		e[ne].nx=f[u];
		f[u]=ne;
	}
}S,T;
int ans,ans2,dp[N][M];
bool vis[N][M];
int qpow(int x,int k)
{	int res=1;
	while(k)
	{	if(k&1)res=1ll*res*x%Mod;
		x=1ll*x*x%Mod;k>>=1;
	}
	return res;
}
void dfs1(int u,int ffa)
{	S.sz[u]=S.bit[u]=0;
	for(int i=S.f[u];i;i=S.e[i].nx)
	{	int v=S.e[i].v;
		if(v==ffa)continue;
		S.son[u][++S.sz[u]]=v;
		dfs1(v,u);
	}
}
void dfs2(int u,int ffa)
{	T.sz[u]=T.bit[u]=0;
	for(int i=T.f[u];i;i=T.e[i].nx)
	{	int v=T.e[i].v;
		if(v==ffa)continue;
		T.son[u][++T.sz[u]]=v;
		dfs2(v,u);
		T.bit[u]|=(1<<(v-1)); 
	}
}
int solve(int u,int v,int A)
{	if(!v)return (!A);
	int t=S.son[u][v];
	if(vis[t][A])return dp[t][A];
	vis[t][A]=1;
	dp[t][A]=solve(u,v-1,A);
	for(int i=0;i>i)&1)dp[t][A]=(dp[t][A]+1ll*solve(u,v-1,A^(1<

LOJ2292 成绩单

因为不知道为什么题解还没写(?)。

#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N=50+5;
int n,A,B,a[N],b[N],g[N][N],f[N][N][N][N];
int main()
{	scanf("%d%d%d",&n,&A,&B);
	for(int i=1;i<=n;i++)
	{	scanf("%d",&a[i]);
		b[i]=a[i];
	} 
	sort(b+1,b+1+n);
	int m=unique(b+1,b+1+n)-b-1;
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(b+1,b+1+m,a[i])-b;
	memset(f,0x3f,sizeof(f));memset(g,0x3f,sizeof(g));
	for(int i=1;i<=n;i++)f[i][i][a[i]][a[i]]=0,g[i][i]=A;
	for(int k=1;k<=n;k++)
	{	for(int i=1;i+k-1<=n;i++)
		{	int j=i+k-1;
			for(int x=1;x<=m;x++)
				for(int y=x;y<=m;y++)
				{	f[i][j][min(x,a[j])][max(y,a[j])]=min(f[i][j][min(x,a[j])][max(y,a[j])],f[i][j-1][x][y]);
					for(int k=i;k

LOJ 6240 仙人掌

求对一个仙人掌随机点分治复杂度的期望。

其中 \(n \le 400\) 。

设 \(p(i,j)\) 表示对于点 \(i\),在 \(j\) 删除前,\(i,j\) 连通的概率。

那么答案可以表示为 \(\sum\limits_{i=1}^n \sum\limits_{j=1}^n p(i,j)\) 。

如果给定的是一棵树,那么有 \(p(i,j)=\dfrac{1}{dis(i,j)}\) 。

那如果有环,那先建一个圆方树。

那么对于圆点,一定不能先选,对于环,一定会有 2 个路径,不能出现两个路径上的点同时被先选。

设 \(i \rightarrow j\) 上走过的环上的点有 \(cnt\) 个,设 \(dp_x\) 表示大小为 \(x\) 满足删除这些点仍满足 \(i,j\) 联合的点集。选 \(j\) 前已经选了 \(k\) 个点。于是有:

\[p(i,j)=\sum\limits_{k=0}^{cnt-1} \dfrac{1}{k+1} \times \dfrac{dp_k}{\tbinom{cnt}{k}} \]

于是可以直接 DP 了。

#include
#include
#include
#include
using namespace std;
typedef long long ll;
const ll N=800+5;
const ll Mod=998244353;
struct edge{
	ll v,nx;
}e[N<<2],e2[N<<2];
ll n,m,ne,ne2,tot,top,f[N],f2[N],dfn[N],low[N],st[N];
ll ans,ex,sz[N],inv[N],C[N][N],ic[N][N],rk[N][N],dp[N][N];
ll qpow(ll x,ll k)
{	ll res=1;
	while(k)
	{	if(k&1)res=res*x%Mod;
		x=x*x%Mod;
		k>>=1;
	}
	return res;
}
void read(ll u,ll v)
{	e[++ne].v=v;
	e[ne].nx=f[u];
	f[u]=ne;
}
void read2(ll u,ll v)
{	e2[++ne2].v=v;
	e2[ne2].nx=f2[u];
	f2[u]=ne2;
}
void dfs(ll u)
{	dfn[u]=low[u]=++tot;
	st[++top]=u;
	for(ll i=f[u];i;i=e[i].nx)
	{	ll v=e[i].v;
		if(!dfn[v])
		{	dfs(v);
			low[u]=min(low[u],low[v]);
			if(dfn[u]==low[v])
			{	ll tmp=0;ex++;
				while(tmp!=v)
				{	tmp=st[top--];
					read2(ex+n,tmp),read2(tmp,ex+n);
					rk[ex][tmp]=++sz[ex];
				}
				read2(ex+n,u),read2(u,ex+n);
				rk[ex][u]=++sz[ex];
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
void solve(ll u,ll ffa,ll cnt)
{	if(u<=n)
	{	for(ll i=0;i<=cnt;i++)ans=(ans+dp[u][i]*ic[cnt+1][i]%Mod*inv[cnt+1-i]%Mod)%Mod;
		for(ll i=f2[u];i;i=e2[i].nx)
		{	ll v=e2[i].v;
			if(v==ffa)continue;
			solve(v,u,cnt);
		}
		return;
	}
	static ll tmp[N][N];
	memcpy(tmp[1],dp[ffa],sizeof(dp[ffa]));
	for(ll i=2;i<=sz[u-n];i++)
	{	memset(tmp[i],0,sizeof(tmp[i]));
		for(ll j=0;j<=cnt+i;j++)
		{	tmp[i][j]=tmp[i-1][j];
			if(j)tmp[i][j]=(tmp[i][j]+tmp[i-1][j-1])%Mod;
		}
	}
	for(ll i=f2[u];i;i=e2[i].nx)
	{	ll v=e2[i].v;
		if(v==ffa)continue;
		ll dis=abs(rk[u-n][ffa]-rk[u-n][v]);
		for(ll j=0;j

[AGC016F] Games on DAG

给定一个 \(n\) 个点 \(m\) 条边的 DAG,对于每条边 \((u,v)\) 都满足 \(u,\(1,2\) 号点各一个石头。

每次可以沿DAG上的边移动一颗石头,不能移动则输,求所有 \(2^m\) 个边的子集中,只保留这个子集先手必胜的方案个数。

其中 \(2 \le n \le 15,1 \le m \le \dfrac{n(n-1)}{2}\) 。

首先可以发现,先手必胜的情况是 \(\operatorname{sg}(1) \neq \operatorname{sg}(2)\) 。所以计算 \(2^m\) 减去两个相等的情况即可。

接下来我们考虑对于所有的 \(\operatorname{sg}(i)=x\) 的连边情况。

首先,对于所有的 \(y ,必须至少向一个 \(\operatorname{sg}(j)=y\) 连边。

其次,他们之间不能相互连边。

最后,对于所有 \(y>x\) ,对于这个 \(\operatorname{sg}(j)=y\) 连也可以不连也可以。

于是考虑从 \(\operatorname{sg}(i)=0\) 开始考虑。同样的,对于所有为 \(0\) 的点,他们之间不能同时连边,剩下的点他们可以随意连。

也就是说,对于一张图,我们可以先把所有 \(\operatorname{sg}(i)=0\) 的点全部导出来,然后再计算剩下的图。而对于剩下的图,我们把所有 \(\operatorname{sg}(i)\) 减去 \(1\) ,于是也变成了导出为 \(0\) 的点。

于是设 \(f(s)\) 表示仅考虑 \(s\) 中的点来导出子图,满足 \(\operatorname{sg}(1)=\operatorname{sg}(2)\) 的连边方案数。此时的 \(1,2 \in s\) 。

转移时候枚举 \(s\) 的子集 \(t\) 。转移过来就可以了。注意 \(t\) 中也要满足 \(\operatorname{sg}(1)=\operatorname{sg}(2)\) 。

分类一下 \(1,2 \in s\) 和 \(1,2 \notin s\) 的情况。

#include
#include
#include
#include
using namespace std;
const int N=15;
const int Mod=1e9+7;
int n,m,pw[N*N],e[N][N];
int g[1<>j&1)j++;
		for(int i=0;i=1;t=(t-1)&s)
		{	if((t&1)!=((t>>1)&1))continue;
			if(t&1)
			{	int res=1;
				for(int i=0;i>i)&1)
					{	if((t>>i)&1)res=1ll*res*(pw[g[s^t][i]]-1)%Mod;
						else res=1ll*res*pw[g[t][i]]%Mod;
					}
				f[s]=(f[s]+1ll*res*f[t]%Mod)%Mod;
			}
			else{
				int res=1;
				for(int i=0;i>i)&1)
					{	if((t>>i)&1)res=1ll*res*(pw[g[s^t][i]]-1)%Mod*pw[g[t][i]]%Mod;
						else res=1ll*res*pw[g[t][i]]%Mod;
					}
				f[s]=(f[s]+res)%Mod;
			}
		}
	}
	printf("%d\n",(pw[m]-f[(1<

300iq Contest 3 H Horrible Cycles

给一个 \(2n\) 个点的二分图,左边第 \(i\) 个点向右边前 \(a_i\) 个点连边,求图中的简单环个数。

其中 \(n \le 5000\) 。

考虑对 \(2n\) 个点排序,使得对于每个左部点都会和前面的右部点连边。考虑每次先编号右部点然后删掉,如果出现一个左部点没有连边那么再给这个左部点标号。

考虑把环拆成链来计算,因为后面的点一定会连到前面的点。设 \(f(i,j)\) 表示对于前 \(i\) 个点,形成 \(j\) 条链的方案数。初始时 \(f(0,0)=1\) 。

如果 \(i\) 是右部点,由于排序所以他和前面都没有连边,所以他可以自己成链。

如果 \(i\) 是左部点,那么他和前面所有的右部点都有连边,所以他会把之前的两条合并到一条。

\[f(i,j) \leftarrow f(i-1,j)+f(i-1,j-1) \\ f(i,j) \leftarrow f(i-1,j)+ j(j-1) f(i,j+1) \]

最后答案要减去二元环的情况,减去所有边即可。然后将答案除以 \(2\) ,因为考虑链的时候是考虑顺序的。

Atcoder CF2017ETR3 F Unicyclic Graph Counting

求 \(n\) 个点的基环树个数,满足第 \(i\) 个点的度数为 \(d_i\)。

其中 \(n \le 300\) 。

首先,如果不看环生成 prufer 序列,那么最后一定会剩下一个环和环旁边的一个点。

对于一个普通的树,如果第 \(i\) 个点度数是 \(d_i\) ,那么答案为 \(\dfrac{(n-2)!}{\prod{(d_i-1)!}}\) 。那么对于基环树,在树上的点就是 \(d_i-1\) 的 出现次数,环上的就是 \(d_i-2\) ,环上和环外的特殊点是 \(d_i-3\) 。

于是有答案 \(\dfrac{(n-|S|-1)!}{\prod(d_i-k)!}\) 。其中 \(k\) 是决定点的类型,\(S\) 是环的集合。

那我们考虑枚举环,但这样显然会 T 。因为同样大小的环本质相同,所以变成枚举环的大小。

于是变成要计算对于一个大小所有的环的 \(\prod(d_i-k)\) 贡献。

这部分考虑 DP ,设 \(f(i,j,k)\) 表示考虑前 \(i\) 个点,有 \(j\) 个在环上,已经有了 \(k=0/1\) 个特殊点。分类直接转移即可。

注意这个 DP 可以用分治 FFT 优化,但原题不需要我没写(?

以及因为 prufer 序列不考虑一个点的情况,所以需要特判只有一个大环的情况。

#include
#include
#include
#include
using namespace std;
typedef long long ll;
const ll N=300+5;
const ll Mod=1e9+7;
ll n,ans,d[N],mul[N],inv[N],fmul[N],f[N][N][2];
int main()
{	scanf("%lld",&n);
	for(ll i=1;i<=n;i++)
		scanf("%lld",&d[i]);
	mul[0]=mul[1]=inv[0]=inv[1]=fmul[0]=fmul[1]=1;
	for(ll i=2;i<=n;i++)
	{	inv[i]=(Mod-Mod/i)*inv[Mod%i]%Mod;
		mul[i]=mul[i-1]*i%Mod,fmul[i]=fmul[i-1]*inv[i]%Mod;
	}
	bool is=1;
	for(int i=1;i<=n;i++)
		if(d[i]!=2)is=0;
	if(is==1)
	{	printf("%lld\n",mul[n-1]*inv[2]%Mod);
		return 0;
	}
	f[0][0][0]=1;
	for(ll i=1;i<=n;i++)
		for(ll j=0;j<=i;j++)
			for(ll k=0;k<2;k++)
			{	if(d[i]>=1)f[i][j][k]=(f[i][j][k]+f[i-1][j][k]*fmul[d[i]-1]%Mod)%Mod;
				if(d[i]>=2)f[i][j][k]=(f[i][j][k]+f[i-1][j-1][k]*fmul[d[i]-2]%Mod)%Mod;
				if(d[i]>=3&&k)f[i][j][1]=(f[i][j][1]+f[i-1][j-1][0]*fmul[d[i]-3]%Mod)%Mod;
			}
	for(ll i=3;i<=n;i++)
		ans=(ans+f[n][i][1]*mul[n-i-1]%Mod*mul[i-1]%Mod*inv[2]%Mod)%Mod;
	printf("%lld\n",ans);
	return 0;
}

300iq Contest 1 J Jealous Split

给一个长度为 \(n\) 的序列,将其划分为 \(k\) 部分,记 \(s_i\) 表示第 \(i\) 部分的和,\(m_i\) 表示第 \(i\) 部分的最大值,构造这样一个划分使得:

\[\forall 1 \le i

其中 \(n \le 10^5\) 。

最后只需要构造,所以考虑一种不合法的情况怎么变成合法。

可以通过调整分界点,在相邻两块最大值不变的情况下,通过这个调整,只会出现和的差变小。那可以考虑维护每段的和的平方的和。

于是题目变成最小化每段和的平方的和,答案是关于 \(k\) 的凸函数,于是可以 wqs 二分。

Yandex Algorithm 2018 Final Round D LIS vs. LDS

给定一个排列,求一组不相交的 LIS 和LDS 。

其中 \(n \le 5 \times 10^5\)。

性质题。首先要发现一个性质,LIS 和 LDS 至多有一个交点。

不妨记 \(f_i\) 表示经过 \(i\) 的 LDS 个数,即从前往后跑 LDS 从后之前跑 LIS 的方案积,再记一个 \(S\) 表示 LDS 总和。

考虑一个 LIS 什么时候一定不合法,也就是不存在一个 LDS 和他没有交点。也就是对于一组 LIS \(\{ i_1,i_2,...,i_k \}\) ,满足其 \(\sum\limits_{j=1}^k f_{i_j}=S\) ,那么一定不合法。

所以只要找一个不满足这个条件的 LIS 即可。

不妨找两个 LIS ,使他们的 \(f\) 和不同,这样最多有一个不合法。

因为这个数字可能很大,所以考虑对一个大质数取膜来优化代码部分(

CF868F Yet Another Minimization Problem

将一个长度为 \(n\) 的序列分为 \(k\) 段,一段 \([l,r]\) 的代价是 \(l \le i < j \le r,a_i=a_j\) 的 \((i,j)\) 对数。最小化分割代价。

其中 \(n \le 10^5,k \le 20\) 。

先考虑暴力 DP ,设 \(f(i,j)\) 表示前 \(i\) 个分了 \(j\) 段的情况,设 \(\operatorname{calc}(k,i)\) 表示区间 \([k,i]\) 的贡献。于是显然有:

\[f(i,j) \leftarrow \min\limits_{k=1}^i \{ f(k-1,j-1) + \operatorname{calc}(k,i) \} \\ \operatorname{calc}(k,i)=\operatorname{calc}(k,i-1)+\sum\limits_{j=k}^{i-1}[a_j=a_i] \]

暴力 DP 过不去就是决策单调。

首先 \(\operatorname{calc}()\) 是单调的。

考虑决策点 \(x,y\) ,其中 \(x 。那么对于 \(i\) 右移前有 \(f(x-1,j-1)+\operatorname{calc}(x,i) > f(y-1,j-1)+\operatorname{calc}(y,i)\) 。那么右移后依然有。

于是决策单调。

因此我们考虑先枚举 \(k\) ,于是可以分治计算。计算每段的贡献类似用莫队的方法计算。 复杂度 \(\mathcal{O}(nk \log n)\) 。

#include
#include
#include
#include
using namespace std;
typedef long long ll;
const ll N=2e5+5;
const ll inf=1e15;
ll n,m,sum,l,r,a[N],cnt[N],dp[N][25];
ll calc(ll L,ll R)
{	while(l>L)sum+=cnt[a[--l]]++;
	while(rR)sum-=--cnt[a[r--]];
	return sum;
}
void solve(ll L,ll R,ll x,ll y,ll dep)
{	ll mid=(L+R)>>1,pos,tl=max(1ll,x),tr=min(mid,y);
	ll mn=inf;
	for(ll i=tl;i<=tr;i++)
	{	ll val=dp[i-1][dep-1]+calc(i,mid);
		if(val

\[\text{by Rainy7} \]