【CF468E】Permanent(DP二合一)

题目链接

  • 一个 \(n\times n\) 的矩阵,初始所有位置填的都是 \(1\)。
  • \(k\) 次操作,每次修改一个位置的值。
  • 所有操作之后,对所有的 \(1\sim n\) 的排列 \(\pi\), 求 \(\prod_{i=1}^na_{i,\pi(i)}\) 之和。
  • \(1\le n\le10^5\),\(1\le k\le50\)

基本转化

把矩阵的行和列看成二分图两侧的点,那么就是要求二分图完美匹配方案数。

初始所有位置填的都是 \(1\),把这看作一般的连边方式。

假设某个位置 \((x,y)\) 填了 \(v\),我们可以认为它是在 \(1\) 的基础上加了 \(v-1\),即存在 \(v-1\) 种特殊的连边方式。

那么就是要选出若干点对表示它们之间选择了特殊连边方式,剩下的点对之间的任意配对只要乘上一个阶乘即可表示。

算法一:状压 DP

特殊点规模可能达到 \(2k\),但任意一个特殊点连通块大小不可能超出 \(k+1\),而它点数较少的那侧的点数肯定小于等于 \(\lfloor\frac{k+1}2\rfloor\)。

而不同连通块之间的贡献可以用背包合并,所以对每个连通块分别 DP。

设 \(f_{i,j}\) 表示 DP 到点数较多的那侧的第 \(i\) 个点,点数较少那侧已经被匹配的点的集合为 \(j\) 的方案数。

显然,通过最终的 \(j\) 就可以知道特殊匹配的点对数。

转移时需要枚举所有边,而边数是 \(O(k)\) 级别的,所以复杂度 \(O(k2^{\frac k2})\)。

算法二:暴枚非树边+树上背包

考虑另一个算法,求出该图的一棵生成树,然后暴力枚举非树边的状态,接着对树边跑树上背包。

枚举非树边复杂度大概是 \(O(2^{k-d})\)(\(d\) 为离散化后的点数)。

树上背包过程中每个点可以不选/与子树内上来的一个点匹配/与父节点匹配,如果在非树边中被选择了就必须不选。它的复杂度是经典的 \(O(d^2)\)。

因此总复杂度 \(O(d^22^{k-n})\)

平衡复杂度

发现算法一的复杂度也可以写成 \()O(d2^{\frac d2})\)。

平衡一下这两个做法的复杂度,主要是针对 \(2\) 的指数 \(\frac d2\) 和 \(k-d\)。因此当 \(d\le \frac23 k\) 的时候做算法一,否则做算法二。

总复杂度 \(O(k^22^{\frac k3})\)

代码:\(O(k^22^{\frac k3})\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Rg register
#define RI Rg int
#define Cn const
#define CI Cn int&
#define I inline
#define W while
#define N 100000
#define K 50
#define X 1000000007
#define add(x,y,z) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].v=z)
using namespace std;
int n,k,Fc[N+5],px[K+5],py[K+5],pv[K+5];
int ee,lnk[2*K+5];struct edge {int to,nxt,v;}e[2*K+5];
struct Discretization
{
	int dc,dv[K+5];I void A(CI v) {dv[++dc]=v;}
	I void Init() {sort(dv+1,dv+dc+1),dc=unique(dv+1,dv+dc+1)-dv-1;}
	I int GV(CI x) {return lower_bound(dv+1,dv+dc+1,x)-dv;} 
}Dx,Dy;
namespace S1//算法一
{
	int ct,G[K+5],sz[1<<K/2];
	int c1,c2,q1[K+5],q2[K+5],vis[2*K+5];void dfs(CI x)//遍历连通块
	{
		vis[x]=1,x<=Dx.dc?q1[++c1]=x:q2[++c2]=x;
		for(RI i=lnk[x];i;i=e[i].nxt) !vis[e[i].to]&&(dfs(e[i].to),0);
	}
	int id[2*K+5],f[2][1<<K/2];I void DP()//状压DP
	{
		RI i,j,p,o,v;if(c1<c2) for(swap(c1,c2),i=1;i<=c1;++i) swap(q1[i],q2[i]);
		RI l=1<<c2;for(i=1;i<=c2;++i) id[q2[i]]=i-1;for(i=1;i^l;++i) f[0][i]=0;
		for(f[0][0]=i=1;i<=c1;++i) for(j=l-1;~j;--j) if(v=f[i&1][j]=f[i&1^1][j])
			for(p=lnk[q1[i]];p;p=e[p].nxt) o=id[e[p].to],!(j>>o&1)&&(f[i&1][j^(1<<o)]=(f[i&1][j^(1<<o)]+1LL*v*e[p].v)%X);//枚举边
		RI t=0;for(i=ct;~i;--i) for(j=1;j^l;++j) G[i+sz[j]]=(G[i+sz[j]]+1LL*G[i]*f[c1&1][j])%X;ct+=c2;//背包合并
	}
	I void Solve()
	{
		RI i,j,p,v,o,l=1<<K/2;for(i=1;i^l;++i) sz[i]=sz[i>>1]+(i&1);
		for(G[0]=i=1;i<=Dx.dc;++i) !vis[i]&&(c1=c2=0,dfs(i),DP(),0);
		RI t=0;for(i=0;i<=ct;++i) t=(t+1LL*G[i]*Fc[n-i])%X;printf("%d\n",t);
	}
}
namespace S2//算法二
{
	int c,F[K+5],G[K+5];struct line {int x,y,v;}s[K+5];
	int vis[2*K+5];void dfs(CI x,CI lst=0)//存下所有非树边
	{
		vis[x]=1;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&
			(vis[e[i].to]?x<e[i].to&&(s[++c]=(line){x,e[i].to,e[i].v},0):(dfs(e[i].to,x),0));
	}
	int fg[2*K+5],f[2*K+5][K+5][2],g[2*K+5];void DP(CI x)//树上背包
	{
		RI i,j,k,y;W(f[x][g[x]][0]=f[x][g[x]][1]=0,g[x]) --g[x];
		for(vis[x]=f[x][0][0]=1,i=lnk[x];i;i=e[i].nxt)
		{
			if(vis[e[i].to]) continue;
			for(DP(y=e[i].to),j=g[x];~j;--j) for(k=g[y];~k;--k)
				k&&(f[x][j+k][0]=(f[x][j+k][0]+1LL*f[x][j][0]*f[y][k][0])%X),
				k&&(f[x][j+k][1]=(f[x][j+k][1]+1LL*f[x][j][1]*f[y][k][0])%X),
				f[x][j+k][1]=(f[x][j+k][1]+1LL*f[x][j][0]*f[y][k][1]%X*e[i].v)%X;
			g[x]+=g[y];
		}
		if(fg[x]) {for(i=0;i<=g[x];++i) f[x][i][1]=0;return;}
		for(i=g[x];~i;--i) f[x][i+1][0]=(f[x][i+1][0]+f[x][i][1])%X,f[x][i][1]=f[x][i][0];f[x][g[x]+1][0]&&++g[x];
		//先前指定的1表示要与子节点匹配(0),0可选择是否与外匹配(0或1)
	}
	int tt,vv;I void Work()
	{
		RI i,j,k,ct=0;for(i=1;i<=Dx.dc+Dy.dc;++i) vis[i]=0;
		for(F[0]=i=1;i<=Dx.dc;++i) if(!vis[i])//背包合并不同连通块
			{for(DP(i),j=ct;~j;--j) for(k=1;k<=g[i];++k) F[j+k]=(F[j+k]+1LL*F[j]*f[i][k][0])%X;ct+=g[i];}
		for(i=0;i<=ct;++i) G[tt+i]=(G[tt+i]+1LL*F[i]*vv)%X,F[i]=0;//注意加上非树边数量,乘上非树边方案数
	}
	void BF(CI x)//暴力枚举非树边是否选择
	{
		if(x>c) return Work();BF(x+1);if(fg[s[x].x]||fg[s[x].y]) return;
		RI v=vv;fg[s[x].x]=fg[s[x].y]=1,++tt,vv=1LL*vv*s[x].v%X,BF(x+1),fg[s[x].x]=fg[s[x].y]=0,--tt,vv=v;
	}
	I void Solve()
	{
		RI i;for(i=1;i<=Dx.dc;++i) !vis[i]&&(dfs(i),0);vv=1,BF(1);
		RI t=0;for(i=0;i<=Dx.dc;++i) t=(t+1LL*G[i]*Fc[n-i])%X;printf("%d\n",t);
	}
}
int main()
{
	RI i;for(scanf("%d%d",&n,&k),Fc[0]=i=1;i<=n;++i) Fc[i]=1LL*Fc[i-1]*i%X;
	for(i=1;i<=k;++i) scanf("%d%d%d",px+i,py+i,pv+i),Dx.A(px[i]),Dy.A(py[i]),--pv[i];//减1,表示特殊匹配方式
	RI x,y;for(Dx.Init(),Dy.Init(),i=1;i<=k;++i) x=Dx.GV(px[i]),y=Dy.GV(py[i]),add(x,Dx.dc+y,pv[i]),add(Dx.dc+y,x,pv[i]);//离散化后建图
	return Dx.dc+Dy.dc<=0.6*k?S1::Solve():S2::Solve(),0;//讨论d与k的大小
}
上一篇:(转)Linux(Centos)之安装Java JDK及注意事项


下一篇:股票的股息与股价对比