返回顶部

高维前缀和(SoSDP)详解及例题(HDU5765)

[toc]

高维前缀和

问题:求n个位的子集或超集上的函数和。例如,\(n=4\),集合$mask=1010$(二进制位串表示,下同),$mask$的子集和是$f(0000) + f(0010) + f(1000) + f(1010)$,超集和是$f(1010) + f(1011) + f(1110) + f(1111)$。
解读一下问题先:

  1. 这个问题可看成是一维部分和、二维部分和在$n$维布尔空间上的推广。
  2. 许多应用需要求子集或超集的个数,这相当于$f$函数是常函数$f(x)=1$的情况。

可用$n2^n$种状态求$n$个位的子集或超集上的函数和,称为SoSDP(Sum of Subset DP)。做法是从最低位开始枚举每一位,再枚举每个数(集合),如果这个数在这一位存在真子集或真超集,就加上这个真子集或真超集的函数和。
以求子集和为例,设$dp_{m,i}(0\le m \le 2n-1,0\le i\le n-1)$表示$m$集合上仅第$0-i$位允许不同的所有子集的函数和。若$i=2,m=1101$,$m$的第2位是1,说明这一位存在真子集$1001$,故$dp_{1101,2} = dp_{1101,1}+dp_{1001,1}$,表示是前一$i$状态的两个$dp$之和,一个$dp$是第$i$位为$1$的子集函数和,另一个$dp$是第$i$位为$0$的子集函数和。相较之下,若$m=1001$,则$dp_{1001,2} = dp_{1001,1}$,表示与前一$i$状态一样。这就是高维前缀和从状态$i-1$转到状态$i$的处理办法。问题最终所求是$dp_{mask,n-1}$。

因为$i$状态的计算仅依赖于$i-1$状态,状态空间可以使用滚动数组并就地更新,压缩到$i$方向只需一行。代码如下:

for (int m = 0; m < (1 << n); m++)
	dp[m] = f(m);
for (int i = 0; i < n; i++) {
	for (int m = 0; m < (1 << n); m++)
		if (m & (1 << i))
			dp[m] += dp[m & ~(1 << i)];

HDU5765

题意: 求无向图所有最小割中每条边的出现次数。
题解: 每个最小割将图分为两个连通块。每个连通块(连通的顶点子集生成子图),若其剩余图(顶点补集的生成子图)也是一个连通块,则两个连通块间的边组成一个最小割,所以枚举所有的连通块能统计出所求次数。需要完成两项任务:

  1. 任意一个顶点子集是否生成连通块?
  2. 已知最小割,如何统计连接两个连通块间的边?

第1项任务用并查集判断每个子集的连通性,计算复杂性是$O(m2n)= O(n22n)$,会超时。状压DP可以提高效率至$O(n2n)$,用二进制位串表示顶点子集。若顶点$i$不在顶点子集$S$,且与$S$中某个点有边,则$S\cup$也是连通块。据此进行状态转移。
第2项任务如果枚举最小割的每条边,计算复杂性也会高达$O(n22n)\(。逆向思维一下,一条边不在最小割的次数如果知道,最小割的总数减去这个次数就是答案。那么边\)(u,v)\(不在最小割有多少次?这个问题等价于在最小割形成的连通块集合中统计\)(u,v)$的超集个数,高维前缀和的SoSDP能$O(n2^n)$时间内解决。具体用$dp_{s,b}$表示限第$0-b$点可变的$s$的超集个数,对所有能由最小割形成的连通块,置初值$dp_{s,-1}=1$。然后使用状态转移方程:
$$dp_{s,b}=\begin
dp_{s,b-1}\qquad b\in s\
dp_{s,b-1}+dp_{s\cup {b},b-1}\qquad b\notin s
\end

\[ 滚动数组优化后代码: ```cpp for (int b = 0; b < n; b++) for (int s = (1<<n) - 1; s >= 0; s--) if (!(s & (1<<b))) dp[s] += dp[s | (1<<b)]; ``` over. **Accept Code:** ```cpp #include <bits/stdc++.h> using namespace std; int n,m; const int maxn=1<<20; int edg[20],u[maxn],v[maxn]; bool c[maxn]; int dp[maxn]; void init(){ memset(c,false,sizeof c); for(int i=0;i<n;i++)c[1<<i]=true; for(int s=0;s<(1<<n);s++){ if(c[s]){ for(int i=0;i<n;i++){ if(!(s&(1<<i))&&(edg[i]&s)){ c[s|(1<<i)]=true; } } } } } int main(){ int t; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ memset(edg,0,sizeof edg); memset(dp,0,sizeof dp); scanf("%d%d",&n,&m); for(int i=0,x,y;i<m;i++){ scanf("%d%d",&x,&y); edg[x]|=(1<<y); edg[y]|=(1<<x); u[i]=x; v[i]=y; } init(); int cnt=0; for(int s=0;s<(1<<n);s++){ int r=(~s)&((1<<n)-1); if(s>r)break; if(c[s]&&c[r]) dp[s]=dp[r]=1,cnt++; } for(int b=0;b<n;b++){ for(int s=0;s<(1<<n);s++){ if(!(s&(1<<b)))dp[s]+=dp[s|(1<<b)]; } } printf("Case #%d:",cas); for (int i = 0; i < m; i++) { printf(" %d",cnt-dp[(1<<u[i])|(1<<v[i])]); } printf("\n"); } return 0; } ``` \]
posted @ 2020-03-17 09:28  Charles1999  阅读(504)  评论(0编辑  收藏  举报