「动态规划 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<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
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<n;i++)
	{	scanf("%d%d%d",&u,&v,&w);
		read(u,v,w),read(v,u,w);
	}
	scanf("%d%d",&st,&m);
	for(int i=1,x;i<=m;i++)
	{	scanf("%d",&x);
		sz[x]++;
	}
	dfs(st,0);
	memset(dp,-1,sizeof(dp));
	ans=inf;
	for(int i=f[st];i;i=e[i].nx)
	{	int v=e[i].v;
		ans=min(ans,DP(i,sz[v],m-sz[v])+e[i].w);
	}
	printf("%d\n",ans);
	return 0;
}

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<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
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<T.n;i++)
		if((A>>i)&1)dp[t][A]=(dp[t][A]+1ll*solve(u,v-1,A^(1<<i))*solve(t,S.sz[t],T.bit[i+1])%Mod)%Mod;
	return dp[t][A];
}
int main()
{	scanf("%d",&S.n);
	for(int i=1,u,v;i<S.n;i++)
	{	scanf("%d%d",&u,&v);
		S.read(u,v),S.read(v,u);
	}
	scanf("%d",&T.n);
	for(int i=1,u,v;i<T.n;i++)
	{	scanf("%d%d",&u,&v);
		T.read(u,v),T.read(v,u);
	}
	dfs1(1,0);
	for(int i=1;i<=T.n;i++)
	{	memset(dp,0,sizeof(dp));
		memset(vis,0,sizeof(vis));
		dfs2(i,0);
		for(int j=1;j<=S.n;j++)ans=(ans+solve(j,S.sz[j],T.bit[i]))%Mod;
	}
	S=T;
	dfs1(1,0);
	for(int i=1;i<=T.n;i++)
	{	memset(dp,0,sizeof(dp));
		memset(vis,0,sizeof(vis));
		dfs2(i,0);
		ans2=(ans2+solve(1,S.sz[1],T.bit[i]))%Mod;
	}
	printf("%lld\n",1ll*ans*qpow(ans2,Mod-2)%Mod);
	return 0;
}

LOJ2292 成绩单

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

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
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<j;k++)f[i][j][x][y]=min(f[i][j][x][y],f[i][k][x][y]+g[k+1][j]);
				}
			for(int x=1;x<=m;x++)
				for(int y=x;y<=m;y++)
					g[i][j]=min(g[i][j],f[i][j][x][y]+A+B*(b[y]-b[x])*(b[y]-b[x]));
		}
	}
	printf("%d\n",g[1][n]);
	return 0;
}

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<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
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<cnt+sz[u-n];j++)
			dp[v][j]=(tmp[dis][j]+tmp[sz[u-n]-dis][j]-dp[ffa][j]+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+sz[u-n]-1);
	}
}
int main()
{	scanf("%lld%lld",&n,&m);
	for(ll i=0;i<=n;i++)
	{	inv[i]=qpow(i,Mod-2);
		C[i][0]=ic[i][0]=1;
		for(ll j=1;j<=i;j++)
		{	C[i][j]=(C[i-1][j]+C[i-1][j-1])%Mod;
			ic[i][j]=qpow(C[i][j],Mod-2);
		}
	}
	for(ll i=1,u,v;i<=m;i++)
	{	scanf("%lld%lld",&u,&v);
		read(u,v);read(v,u);
	}
	dfs(1);
	for(ll i=1;i<=n;i++)
	{	memset(dp,0,sizeof(dp));
		dp[i][0]=1;
		solve(i,0,0);
	}
	printf("%lld\n",ans);
	return 0;
}

[AGC016F] Games on DAG

给定一个 \(n\) 个点 \(m\) 条边的 DAG,对于每条边 \((u,v)\) 都满足 \(u<v\)\(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<x\) ,必须至少向一个 \(\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<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=15;
const int Mod=1e9+7;
int n,m,pw[N*N],e[N][N];
int g[1<<N][N],f[1<<N];
int main()
{	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;i++)
	{	scanf("%d%d",&u,&v);
		e[u-1][v-1]=1;
	}
	pw[0]=1;
	for(int i=1;i<=m;i++)pw[i]=pw[i-1]*2%Mod;
	for(int s=1;s<(1<<n);s++)
	{	int j=0;
		while(~s>>j&1)j++;
		for(int i=0;i<n;i++)g[s][i]=g[s^(1<<j)][i]+e[i][j];
	}
	for(int s=0;s<(1<<n);s++)
	{	if((s&3)!=3)continue;
		f[s]=1;
		for(int t=s&(s-1);t>=1;t=(t-1)&s)
		{	if((t&1)!=((t>>1)&1))continue;
			if(t&1)
			{	int res=1;
				for(int i=0;i<n;i++)
					if((s>>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<n;i++)
					if((s>>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<<n)-1]+Mod)%Mod);
	return 0;
}

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<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
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 <k,|s_i-s_{i+1}| \le \max(m_i,m_{i+1}) \]

其中 \(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<y\) 。那么对于 \(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<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
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(r<R)sum+=cnt[a[++r]]++;
	while(l<L)sum-=--cnt[a[l++]];
	while(r>R)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<mn)mn=val,pos=i;
	}
	dp[mid][dep]=mn;
	if(L==R)return;
	solve(L,mid,x,pos,dep),solve(mid+1,R,pos,y,dep);
}
int main()
{	scanf("%lld%lld",&n,&m);
	for(ll i=1;i<=n;i++)dp[i][0]=inf;
	for(ll i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	cnt[0]=1;
	for(ll i=1;i<=m;i++)solve(1,n,1,n,i);
	printf("%lld\n",dp[n][m]);
	return 0;
}

\[\text{by Rainy7} \]

posted @ 2022-02-22 01:15  Rainy7  阅读(168)  评论(0编辑  收藏  举报