1. 程式人生 > 其它 >【CF468E】Permanent(DP二合一)

【CF468E】Permanent(DP二合一)

一個 $n\times n$ 的矩陣,初始所有位置填的都是 $1$。$k$ 次操作,每次修改一個位置的值。所有操作之後,對所有的 $1\sim n$ 的排列 $\pi$, 求 $\prod_{i=1}^na_{i,\pi(i)}$ 之和。

題目連結

  • 一個 \(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的大小
}
敗得義無反顧,弱得一無是處