状压 DP 笔记汇总

A P5911 [POI2004] PRZ\(\ _{\text{By}\ \text{yxans}}\)

签到题。考虑记 \(f_i\) 表示状态为 \(i\) 时的最小花费,枚举子集判断子集 \(w\) 之和只要不超过 \(W\) 就转移,因此要预处理每个集合的 \(w\) 之和,\(t\) 最大值。

转移方程是:

\[f_m \gets \min_{s \subset m}f_s+T_s \]

data
  MW = read(), n = read();

  if (F) cerr << MW << ' ' << n << '\n';

  vector<int> t(n + 1), w(n + 1);
  vector<int> T((1 << n) + 100), W((1 << n) + 100), f((1 << n) + 100);

  for (int i = 0; i < n; i++) t[i] = read(), w[i] = read();

  for (int m = 0; m < (1 << n); m++) {
    f[m] = inf;
    for (int i = 0; i < n; i++) {
      if ((m >> i) & 1) {
        T[m] = max(T[m], t[i]);
        W[m] += w[i];
      }
    }
    if (F) printf("m:%d W[%d]:%d\n", m, m, W[m]);
  }

  f[0] = 0;

  for (int m = 0; m < (1 << n); m++) {
    for (int s = m; s; s = (s - 1) & m) {
      if (W[s] > MW) continue;
      if (F)
        if (m == 5)
          cerr << s << ' ' << f[m ^ s] << ' ' << f[m ^ s] + T[s] << ' ' << W[s]
               << '\n';
      f[m] = min(f[m], f[m ^ s] + T[s]);
    }
    if (F) printf("m:%d f[%d]:%d\n", m, m, f[m]);
  }

  cout << f[(1 << n) - 1];

  return 0;

B [JSOI2009] 暗証番号(密码)\(\ _{\text{By}\ \mathfrak{lihe\_qwq}}\)

题意简述:现有一长度为 \(L\) 的未知字符串,给你若干他的连续字串,问原字符串有几种可能,若可能性少于 \(42\),输出按字典序输出所有可能。

数据范围:对于 \(100\%\) 的数据,\(1≤L≤25\)\(1≤N≤10\),每个连续字串长不超过 \(10\),并且保证输出结果小于 \(2^63\)

转换题意,若干个串,按 \(xyh+zen+qiang+jiao+wo+quan+ke=xyhzenqiangjiaowoquanke\) 的规则拼在一起,最终拼出一个长度为 \(L\) 的串。发现这玩意很像多模式匹配,所以考虑 AC 自动机求 \(fail\)

\(dp\) 状态:\(dp_{i,j,k}\) 表示当前拼完了原字符串的前 \(i\) 位,在 trie 树上位置为 \(j\),二进制状压已经用完的串为状态 \(k\)

显然的方程

\[\large {dp_{i+1,t_{j,c},k|tag_{t_{j,c}}}\gets dp_{i,j,k}} \]

tag 表示当前状态已经包含的字符串,可以在自动机的时候处理出来,\(0\le c\le 25\)

至于输出字符串,由于数量较少,暴力即可。

code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+10;
int n,m,ans;
int t[N][27],cnt,tag[N],fal[N];
char s[20][20];
void insert(char *s,int id)
{
    int x=0,len=strlen(s);
    for(int i=0;i<len;i++)
    {
    	int c=s[i]-'a';
    	if(!t[x][c])
            t[x][c]=++cnt;
    	x=t[x][c];
    }
    tag[x]|=(1<<id);
}
void AC()
{
    queue<int>q;
    fal[0]=0;
    for(int i=0;i<26;i++)
        if(t[0][i])
            q.push(t[0][i]);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        tag[u]|=tag[fal[u]];
        for(int i=0;i<26;i++)
            if(!t[u][i])
                t[u][i]=t[fal[u]][i];
            else
                fal[t[u][i]]=t[fal[u]][i],q.push(t[u][i]);
    }
}
int f[30][105][1<<11],pd[30][30];
void pre()
{
    for(int x=1;x<=m;x++)
        for(int y=1;y<=m;y++)
        {
            if(x==y)    continue;
            int lx=strlen(s[x]),ly=strlen(s[y]);
            for(int len=min(lx,ly);len>=0;len--)
            {
                bool fl=0;
                for(int i=0;i<len;i++)
                {
                    int j=lx-len+i;
                    if(s[x][j]!=s[y][i])
                    {
                        fl=1;
                        break;
                    }
                }
                if(!fl)
                {
                    pd[x][y]=len;
                    break;
                }
            }
        }
}
string tmp,mm[50];
int lp[30],vis[30],tot;
void dfs(int x)
{
    if(x==m+1)
    {
        tmp.clear();
        for(int i=1;i<=m;i++)
        {
            int st=0,len=strlen(s[lp[i]]);
            if(i!=1)
                st=pd[lp[i-1]][lp[i]];
            for(int j=st;j<len;j++)
                tmp+=s[lp[i]][j];
        }
        if(tmp.size()==n)
            mm[++tot]=tmp;
        return;
    }
    for(int i=1;i<=m;i++)
    {
        if(vis[i])  continue;
        lp[x]=i,vis[i]=1;
        dfs(x+1);
        vis[i]=0,lp[x]=0;
    }
}
signed main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%s",s[i]);
        insert(s[i],i-1);
    }
    AC();f[0][0][0]=1;
    for(int i=0;i<n;i++)
        for(int j=0;j<=cnt;j++)
            for(int k=0;k<(1<<m);k++)
                if(f[i][j][k])
                    for(int c=0;c<26;c++)
                        f[i+1][t[j][c]][k|tag[t[j][c]]]+=f[i][j][k];
    for(int i=0;i<=cnt;i++)
        ans+=f[n][i][(1<<m)-1];
    printf("%lld\n",ans);
    if(ans<42)
    {
        pre();dfs(1);
        sort(mm+1,mm+ans+1);
        for(int i=1;i<=ans;i++)
            cout<<mm[i]<<'\n';
    }
    return 0;
}

C [SDOI2009] 学校の食堂(学校食堂)\(\ _{\text{By}\ \mathfrak{lihe\_qwq}}\)

题意简述:一群人吃饭,食堂每次只能给一个人做饭,第 \(i\) 个人要的饭有一个值 \(T_i\),食堂给他做饭花的时间是上一个人 \(j\)\(T_j\ \^{}\ T_i\)。每一个人 \(i\) 还有一个限制 \(B_i\),表示最多他后面紧跟着的 \(B_i\) 个人(即 \([i+1,i+B_i]\))比他先拿到饭,否则则是非法,问所有人都拿到饭的最短时间。

数据范围:\(1≤N≤1000\)\(0≤T_i≤1000\)\(0≤B_i≤7\)\(1≤C≤5\)\(C\)是多测)。

dp状态:首先第一维记录轮到了第 \(i\) 个人(即前 \(i-1\) 个人已经全都吃上饭了)发现 \(B_{i}\) 非常小,考虑状压记录第 \(i\) 人以及其后面 \(B_{i}\) 个人的状态,转移值与前一个人的 \(T\) 有关,考虑再开一维 \(k\) \((-8\leq k\leq 7)\) 表示上一个打饭的人是 \(i+k\)

所以状态为 \(f_{i,j,k}\) 表示前 \(i-1\) 个人已经处理完了,到了第 \(i\) 个人,第 \(i\) 人及其后面 \(B_{i}\) 人的状态为 \(j\) ,上一个上一个给饭的人为 \(k\)

很容易有两个方程

\[\begin{cases} \large {f_{i+1,j/2,k-1}}&\large \gets {\min(f_{i+1,j/2,k-1},f_{i,j,k})} \\ \large {f_{i,j|2^h,h}}&\large \gets {\min(f_{i,j|2^h,h},f_{i,j,k}+t_{i+h}\ \^{}\ t_{i+k})(0\leq h\leq 7)} \end{cases} \]

code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+10;
int T,n,t[N],b[N],f[1005][1<<9][20],inf,lim;
signed main()
{
    scanf("%lld",&T);
	while(T--)
	{
		scanf("%lld",&n);
		for(int i=1;i<=n;i++)
			scanf("%lld%lld",&t[i],&b[i]);
		memset(f,0x3f,sizeof(f));inf=f[0][0][0];
        f[1][0][7]=0;
		for(int i=1;i<=n;i++)
			for(int j=0;j<(1<<8);j++)
				for(int k=-8;k<=7;k++)
				{
					if(f[i][j][k+8]==inf)	continue;
					if(j&1)
						f[i+1][j>>1][k+7]=min(f[i+1][j>>1][k+7],f[i][j][k+8]);
                    else
                    {
                        lim=inf;
                        for(int h=0;h<=7;h++)
                        {
                            if((j>>h)&1)    continue;
                            if(i+h>lim) break;
                            lim=min(lim,i+h+b[i+h]);
                            f[i][j|(1<<h)][h+8]=min(f[i][j|(1<<h)][h+8],f[i][j][k+8]+(i+k?(t[i+k]^t[i+h]):0));
                        }
                    }
				}
        int ans=inf;
        for(int i=-8;i<=7;i++)
            ans=min(ans,f[n+1][0][i+8]);
        printf("%lld\n",ans);
	}
	return 0;
}

D [省选联考 2020 A/B 卷] 信号の伝送(信号传递)\(\ _{\text{By}\ \mathfrak{lihe\_qwq}}\)

题意简述:现在有若干个信号塔,两两相隔距离为 \(1\),和若干个传输任务,对于要位于 \(x\) 的信号塔传信号至位于 \(y\) 的信号塔的任务花费为:若 \(x\leq y\) ,花费为 \(y-x\),若 \(x>y\),花费为 \(xk+yk\)\(k\) 为一给定值),现允许你将信号塔随便调换位置,问完成所有任务的最小花费。

数据范围:\(2\leq m \leq 23\)\(2\leq n \leq 10^5\)\(1\leq k \leq 100\)\(1\leq S_{i}\leq m\)

首先考虑一个性质,每次的花费可以分配到每个点上,然后统计答案时进行累加即可,例:对于一个坐标为 \(x\) 的点,有若干个 \(x\rightarrow y\)\(y\rightarrow x\) 的任务,对于 \(x\rightarrow y\) 的任务,若 \(x>y\),则 \(x\) 的代价增加 \(k\),若 \(x\leq y\),则 \(x\) 的代价增加 \(-1\),同理对于 \(y\rightarrow x\) 的任务有贡献 \(1\)\(k\),最后计算总代价时乘上其坐标而后求和便可。

则可设 dp 状态为 \(f_{S}\),状压已经用过的点的状态为 \(S\),则转移式 \(f_{S|2^i}=f_{S}+pre_{i,S}\),pre数组可以暴力预处理出来,现唯一的问题是这样做的话内存复杂度是 \(m2^m\geq 23 \times 2^{23} \times 4 B \approx 736MB\),会爆内存。考虑当 \(i\) 属于 \(S\) 时这样的状态是没意义的,则可以优化掉一个 2,则内存 \(\approx 368MB\),足以跑过。

code:
#include<bits/stdc++.h>
using namespace std;
const int M=23,N=1<<23;
int n,m,K,lg[N],cnt[N],g[M][M],h[M][N>>1],f[N],inf=1e9;
signed main()
{
    scanf("%d%d%d",&n,&m,&K);lg[0]=-1;
    int la=0;
    for(int i=1,x;i<=n;i++)
    {
        scanf("%d",&x);x--;
        if(i==1)   
        {
            la=x;
            continue;
        }
        g[la][x]++;la=x;
    }
    for(int i=1;i<(1<<m);i++)
        lg[i]=lg[i>>1]+1,cnt[i]=cnt[i>>1]+(i%2==1);
    for(int i=0;i<m;i++)
    {
        for(int j=0;j<m;j++)
            if(i!=j)
                h[i][0]+=g[j][i]*K-g[i][j];
        for(int j=1,x,bh;j<(1<<(m-1));j++)
        {
            x=j&-j;bh=lg[x];bh+=(bh>=i);
            h[i][j]=h[i][j^x]+g[i][bh]*(1+K)+g[bh][i]*(1-K);
        }
    }
    for(int i=1;i<(1<<m);i++)
    {
        f[i]=inf;
        for(int j=i,bh,x,y;y=j&-j;j^=y)
        {
            bh=lg[y],x=i^y;
            f[i]=min(f[i],f[x]+h[bh][x&y-1|x>>bh+1<<bh]*cnt[i]);
        }
    }
    printf("%d",f[(1<<m)-1]);
    return 0;
}

E P7519 [省选联考 2021 A/B 卷] 滚榜\(\ _{\text{By}\ \text{yxans}}\)

首先考虑一个朴素的 dp:设 \(f_{m,i,j,k}\) 表示当前分过 \(m\) 的人的集合是 \(m\),上一次分 \(m\) 的人是 \(i\),给它分了 \(j\) ,已经用了 \(k\)。这样设置 dp 的原因是上一次分 \(m\) 的大小对我们当前分 \(m\) 的大小有影响( \(b_i\) 单调不下降),转移是简单的。空间复杂度是 \(O(2^nnm^2)\) ,时间复杂度为 \(O(2^nn^2m^2)\) ,获得了一个比阶乘还慢的复杂度。

我们考虑先把空间优化下去。因为我们只关注排名的方案数,对于 \(b_i\) 的大小我们实际上并不关心,只是为了后面的转移而必须记录。所以我们感觉上应该把第三维省掉。

不妨先考虑对于一种排名的方式: \((p1,p2,p3,...,pn)\) ,这样的排名会成立当且仅当存在一种分配 \(m\) 的方式使得其满足条件。如果存在一种最优秀的分配方式,这种分配方式下条件满足,我们就对答案加 \(1\),反之这种排名是不成立的。

贪心的策略是简单的。考虑这个排列的任意连续的两位 \(p_{i−1},p_i\) ,我们让 \(i\) 成为第一名的时候,贪心选择最少的 \(b_i\) 分配给它使之成为第一名。这样我们就可以留下更多的 \(b\) 分配给后面,这个排名就有更大的概率成立。

\(p_{i-1}\) 的排名小于 \(p_i\) 时,有: \(a_{i-1}+b_{i-1}+1=a_i+b_i\)

\(p_{i-1}\) 的排名不小于 \(p_i\) 时,有: $a_{i-1}+b_{i-1}+1=a_i+b_i $

有了这个之后我们就可以将费用提前了。记 \(d_{i,j}\) 表示上一个选 \(i\),这次选 \(j\) 的最小的 \(b\) 值,一个数实际被选做第一名时的大小为 \(\sum d_{p_{i-1},p_i}\) ,所以我们选择一个数,假设它是第 \(i\) 个被选的,它的 \(b\) 值贡献了 \(n-i+1\) 次。

这样做每次的 \(b\) 的选择都是唯一确定的,就不需要枚举上一次的 \(b\) 值来确定这次 \(b\) 的范围了。空间复杂度:\(O(2^nnm)\) ,时间复杂度 \(2^nn^2m\) ,可以通过。

code
  n = read(), m = read();

  for (int i = 1; i <= n; i++) a[i] = read();

  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++) d[i][j] = max(0, a[i] - a[j] + (j > i));

  for (int j = 1; j <= n; j++)
    for (int i = 1; i <= n; i++) s[j] = max(s[j], d[i][j]);

  for (int i = 1; i < (1 << n); i++) pcnt[i] = pcnt[i >> 1] + (i & 1);

  for (int i = 1; i <= n; i++)
    if (n * s[i] <= m) f[1 << i - 1][i][n * s[i]] = 1;

  for (int s = 0; s < (1 << n); s++) {
    for (int i = 1; i <= n; i++)
      if (s >> i - 1 & 1) {
        int A = s ^ (1 << i - 1), num = n - pcnt[A];
        for (int j = 1; j <= n; j++)
          if (i != j && (s >> j - 1 & 1)) {
            for (int t = d[j][i] * num; t <= m; t++)
              f[s][i][t] += f[A][j][t - d[j][i] * num];
          }
      }
  }

  Yc ans = 0;

  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++) ans += f[(1 << n) - 1][i][j];

F P4363 [九省联考 2018] 一双木棋 chess\(\ _{\text{By}\ \mathfrak{whrwlx}}\)

轮廓线 DP +记忆化

我们可以发现一个位置能落子,当且仅当左上角的矩形内部 只有 自己一个空位

由此可得出状态是类似 阶梯形

那么我们考虑轮廓线 DP:

不妨用 \(1\) 表示竖边,\(0\) 表示横边

就可以用二进制表示出当前状态

\(f[msk]\) 表示这个轮廓线状态 距游戏结束 还能得多少分

状态的转移就是把第 \(i\) 位上的 \(1\) 左移一位即可

这个操作在位运算中可以写为 msk^(3<<i)

注意:边界条件是 f[((1<<n)-1)<<m]=0

Code
#include<bits/stdc++.h>
#define int long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;

inline int read()
{
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
	return x*f;
}

const int N=5+9,M=2e6+509,mod=998244353;

int n,m;
int a[N][N],b[N][N];
unordered_map<int,int> f;
/*
轮廓线DP
一个位置能落子,当且仅当左上角的矩形内部只有自己一个空位
用 1 表示竖边,0 表示横边
状态的转移就是把其中一个 1 左移一位即可
本质就是 01−>10
令 f[msk] 表示这个轮廓线状态距游戏结束还能得多少分
可以得到边界条件 f[((1<<n)-1)<<m]=0
*/

int F(int msk,bool id)
{
	if(f.count(msk)) return f[msk];
	f[msk]=id?-1e18:1e18;
	int x=n,y=0;
	fd(i,0,n+m-2)
	{
		if(msk>>i&1) x--;else y++;//延轮廓线递推
		if((msk>>i&3)!=1) continue;//没空位
		int nxt=msk^(3<<i);//把 1 左移
		if(id) f[msk]=max(f[msk],F(nxt,id^1)+a[x][y]);
		else f[msk]=min(f[msk],F(nxt,id^1)-b[x][y]);
	}
	return f[msk];
}

signed main()
{
//#define FJ
#ifdef FJ
	freopen("color2.in","r",stdin);
	freopen("color.out","w",stdout);
#endif
//#define io
#ifdef io
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
#endif
	
	n=read(),m=read();
	fd(i,0,n-1) fd(j,0,m-1)
		a[i][j]=read();

	fd(i,0,n-1) fd(j,0,m-1)
		b[i][j]=read();
	
	f[((1<<n)-1)<<m]=0;
	printf("%lld",F((1<<n)-1,1));
	
    return 0;
}

G [NOI2015] 寿司のディナーパーティー(寿司晚宴)\(\ _{\text{By}\ \mathfrak{lihe\_qwq}}\)

题目简述:现有 \(n-1\) 个寿司,美味度为依次为 \(2,3,4,……,n-1,n\),现有两个人,每个人可以不吃或吃若干个寿司,两个人所吃的任意两个寿司互质,问有多少方案,方案数对 \(p\) 取模(\(p\) 给定)

首先,有一个暴力做法,将每一个质因子全部存起来即 \(f_{i,S_1,S_2}\) 表示分配过第 \(i\) 个寿司,第一个人已选的质因子压缩状态为 \(S_1\),第二个人已选的质因子压缩状态为 \(S_2\),则有方程:

\[\begin{cases} f_{i,S_1|k,S_2}\gets f_{i−1,S_1,S_2}&k\&S_2=0 \\ f_{i,S_1,S_2|k}\gets f_{i−1,S_1,S_2}&k\&S_1=0 \end{cases} \]

显然这样只能跑过 \(n\leq 30\) 的数据。

考虑对暴力做法进行优化,当 \(n\leq 500\) 时,一个数其最多只有一个大于等于 \(23\) 的质因子,考虑将其单独记录,余下的质因子只剩下了 \(8\) 个,则余下的 \(8\) 个小质因子则可以状压来做。具体做法是将 \(2\)\(n\) 的每个数都处理出其大于等于 \(23\) 的质因子与所包含的小质因子,并将大质因子相同的放在一起,再记录两个数组 \(f1_{S_1,S_2}\)\(f2_{S_1,S_2}\),每一次使 \(f1=f2=f\),然后计算当前大质因子分别分给两人的方案,然后合并 \(f_{S_1,S_2}=f1_{S_1,S_2}+f2_{S_1,S_2}-f_{S_1,S_2}\)

code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+10;
int n,mod,f[305][305],f1[305][305],f2[305][305],ans;
int p[305]={0,2,3,5,7,11,13,17,19};
struct node
{
    int val,B,S;
}a[505];
void pre(int id)
{
    int tmp=a[id].val;a[id].B=-1;
    for(int i=1;i<=8;i++)
    {
        if(tmp%p[i]) continue;
        a[id].S|=(1<<i-1);
        while(tmp%p[i]==0)
            tmp/=p[i];
    }
    if(tmp!=1)
        a[id].B=tmp;
    return; 
}
bool cmp(node x,node y)
{
    return x.B<y.B;
}
int pl(int l,int r)
{
    return l+r>=mod?l+r-mod:l+r;
}
signed main()
{
    scanf("%lld%lld",&n,&mod);
    for(int i=1;i<n;i++)
        a[i].val=i+1,pre(i);
    sort(a+1,a+n,cmp);f[0][0]=1;
    for(int i=1;i<n;i++)
    {
        if(i==1||a[i].B!=a[i-1].B||a[i].B==-1)
            for(int j=0;j<=(1<<8)-1;j++)
                for(int k=0;k<=(1<<8)-1;k++)
                    f1[j][k]=f2[j][k]=f[j][k];
        for(int j=(1<<8)-1;j>=0;j--)
            for(int k=(1<<8)-1;k>=0;k--)
                if((j&k)==0)
                {
                    if((k&a[i].S)==0)
                        f1[j|a[i].S][k]=pl(f1[j|a[i].S][k],f1[j][k]);
                    if((j&a[i].S)==0)
                        f2[j][k|a[i].S]=pl(f2[j][k|a[i].S],f2[j][k]);
                }
        if(i==n-1||a[i].B!=a[i+1].B||a[i].B==-1)
            for(int j=0;j<=(1<<8)-1;j++)
                for(int k=0;k<=(1<<8)-1;k++)
                    if((j&k)==0)
                        f[j][k]=pl(f1[j][k],pl(f2[j][k],mod-f[j][k]));
    }
    for(int j=0;j<=(1<<8)-1;j++)
        for(int k=0;k<=(1<<8)-1;k++)
            if((j&k)==0)
                ans=pl(ans,f[j][k]);
    printf("%lld",ans);
    return 0;
}

H P5616 [MtOI2019] 恶魔之树\(\ _{\text{By}\ \mathfrak{whrwlx}}\)

Hash+DP

显然题意可以转化为求 所有子序列的 \(\operatorname{lcm}\) 之和

注意到 \(1\le s_i\le 300\)去重,设值的个数为 \(m\) 个,并记录每种值的出现次数 \(cnt_i\)

然后可以把质数分为 \(\le \sqrt{300}\)\(> \sqrt{300}\) 两部分

发现在所有 \(s_i\) 中只会出现 至多一个 大质数 \(P_i(P_I>\sqrt{300})\),以每个 \(s_i\)\(P_i\) 为关键字排序(没有就是 \(1\)),令新数组为 \(New\)

然后 \(New\) 就被分成了若干段,每段都有同一个 \(P_i\)

\(f_{i,j,0/1}\) 为遍历到 \(New_{i}\),小质数的 \(\operatorname{lcm}\)\(j\)\(P_{i}\) 没选/选时,所有大质数对答案的贡献

下令 \(j'=\operatorname{lcm}(j,\frac{New_{i}}{P_{i}})\)

初始\(f_{0,1,0}=1\)

\(P_{i}=P_{i-1}\),则

\[\begin{cases} f_{i,j,0}\gets f_{i,j,0}+f_{i-1,j,0}\\ f_{i,j,1}\gets f_{i,j,1}+f_{i-1,j,1}\\ f_{i,j',1}\gets f_{i,j',1}+(f_{i-1,j,0}\times P_{i} +f_{i-1,j,1})(2^{cnt_{New_{i}}}-1) \end{cases} \]

否则

\[\begin{cases} f_{i,j,0}\gets f_{i,j,0}+f_{i-1,j,0}+f_{i-1,j,1}\\ f_{i,j',1}\gets f_{i,j',1}+P_{i}(f_{i-1,j,0}+f_{i-1,j,1})(2^{cnt_{New_{i}}}-1) \end{cases} \]

最后答案就是 \(\sum j(f_{m,j,0}+f_{m,j,1})\)

式中的 \(j\) 直接作下标会 RE,所以考虑 Hash(预处理所有 \(\operatorname{lcm}\)

\(\sqrt{300}\) 小的质数集合 \(P\)\(\{2,3,5,7,11,13,17\}\),所以状态总数为 \(\prod_{p \in P}(k_{p} +1)=17496\)

注意:最好不要用 %mod,建议用手写模

Code
#include<bits/stdc++.h>
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;

inline int read()
{
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
	return x*f;
}

const int N=3e5+5,M=305,K=2e4+5;

int n,mod,a[N];
int Lcm[K],p[8]={0,2,3,5,7,11,13,17},k=7,pw[N];//lcm prime pow2
int tot,cnt[M],m,f[M][K][2],ans;
unordered_map<int,int> h;//hash
pair<int,int> b[N],t[M];

inline int add(int x,int y)
{
	return x+y>=mod?x+y-mod:x+y;
}

inline int mul(int x, int y)
{
	return (int)(1ll*x*y%mod);
}

int gcd(int x,int y)
{
	return y==0?x:gcd(y,x%y);
}

inline int lcm(int x,int y)
{
	return x/gcd(x,y)*y;
}

void init()
{
	fd(i,1,n) a[i]=read();
	
	fd(i,1,n)
	{
		b[i]={1,1};
		fd(j,1,k)
		{
			while(a[i]%p[j]==0)
			{
				a[i]/=p[j];
				b[i].second*=p[j];
			}
		}
		b[i].first=a[i];
	}
	
	sort(b+1,b+n+1);
	
	fd(i,1,n)//去重
	{
		if(b[i]!=b[i-1])
			t[++m]=b[i];
		cnt[m]++;
	}
}

void pre()
{
	for(int i2=1;i2<=300;i2*=2)
		for(int i3=1;i3<=300;i3*=3)
			for(int i5=1;i5<=300;i5*=5)
				for(int i7=1;i7<=300;i7*=7)
					for(int i11=1;i11<=300;i11*=11)
						for(int i13=1;i13<=300;i13*=13)
							for(int i17=1;i17<=300;i17*=17)
							{
								int x=1ll*i2*i3*i5*i7*i11*i13*i17;
								Lcm[++tot]=x,h[x]=tot;
							}
	
	pw[0]=1;
	fd(i,1,n) pw[i]=add(pw[i-1],pw[i-1]);
}

void DP()
{
	f[0][h[1]][0]=1;
	fd(i,1,m)
	{
		if(t[i].first==t[i-1].first)
		{
			fd(j,1,tot)
			{
				int x=lcm(Lcm[j],t[i].second);
				f[i][j][0]=add(f[i][j][0],f[i-1][j][0]);
				f[i][j][1]=add(f[i][j][1],f[i-1][j][1]);
				f[i][h[x]][1]=add(f[i][h[x]][1],mul(add(mul(f[i-1][j][0],t[i].first),f[i-1][j][1]),pw[cnt[i]]-1));
			}
		}
		else
		{
			fd(j,1,tot)
			{
				int x=lcm(Lcm[j],t[i].second);
				f[i][j][0]=add(f[i][j][0],add(f[i-1][j][0],f[i-1][j][1]));
				f[i][h[x]][1]=add(f[i][h[x]][1],mul(mul(add(f[i-1][j][0],f[i-1][j][1]),t[i].first),pw[cnt[i]]-1));
			}
		}
	}
	fd(i,1,tot) ans=add(ans,mul(add(f[m][i][0],f[m][i][1]),Lcm[i]%mod));
}

signed main()
{
// #define FJ
#ifdef FJ
	freopen("A.in","r",stdin);
	freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
#endif
	
	n=read(),mod=read();
	pre();
	init();
	DP();
	printf("%lld",ans);
	
	return 0;
}

I P4649 [IOI2007] training 训练路径\(\ _{\text{By}\ \text{yxans}}\)

将每一条非树边分成两种情况:两端点到 lca 的路径与这条边组成的环为奇/偶环。

如果组成的环是偶环,那么这条非树边一定要被选择。除去所有这样的边,剩下的图的每一个简单环都是奇环。

考虑对于其中的任意两个树上部分有交的简单环,我们把这两个环拼到一起然后删去树上的公共部分能得到的环一定是一个偶环。

如图中蓝色部分和红色部分分别是一个简单环,将两环拼接,删去紫色部分(公共部分)得到的纯蓝色和纯红色部分拼起来的是一个偶环。

原因是原来的两奇环相加为偶数,减去了紫色部分 \(\times 2\) ,所以得到的剩余部分是偶环。

于是最终的图上不能存在一条树边,使得它同时存在于两个简单环中。容易发现加上这条限制后得到的图一定是合法的(所有简单环都是奇环,并且各个简单环还无交,这种情况下所有的环均是简单环,因为各个点最多经过一次且各条边最多经过一次)。

我们先判掉直接形成偶环的非树边,原问题等价于在新图中删去权值和更小的边使得每条边最多存在于一个环中。

删边判环是困难的,我们考虑把问题反过来,转化为在树上加入权值和更大的边使得每条边最多存在于一个环中。

\(f_{i,j}\) 表示以 \(i\) 为根的子树,不考虑 \(j\) 集合里的儿子子树,边权和的最大值。

那么 \((u,v)\) 这条非树边肯定是在 \(lca_{u,v}\) ,被处理。对于 \(f_{i,j}\) ,首先有:\(f_{i,j}=\sum_{v\notin j}f_{v,0}\)

我们枚举挂在 \(i\) 上的每一条非树边,不选显然是不需要做处理的,如果选的话,转移为:

\[f_{i,j}=f_{i,j|2^x|2^y}+f_{u,0}+f_{v,0}+w_{u,v}+\sum_{t\neq i,fa_t\neq i}f_{fa_t,id_t} \]

PS:这里的斜线不知道为什么飞外面了,\(\not =\)是不等于,\(\not \in\) 是不属于

对于这个式子的第四部分,我们提前 \(O(m)\) 的预处理一下,所以复杂度是 \(O(m2^n+mn)\)

data
  void dfs(int u, int Fa) {
  vis[u] = true, fa[u] = Fa, dep[u] = dep[Fa] + 1;

  int cntid = 0;
  for (int i = h[u]; i; i = e[i].nxt) {
    int v = e[i].v, w = e[i].w;
    if (vis[v] || w) continue;

    e[i].id = e[i ^ 1].id = cntid++;
    pos[u][e[i].id] = i;
    dfs(v, u);
  }
}

void pre_lca() {
  for (int i = 1; i <= n; i++) F[i][0] = fa[i];
  for (int j = 1; j <= lg[n]; j++)
    for (int i = 1; i <= n; i++) F[i][j] = F[F[i][j - 1]][j - 1];
}

pii lca(int u, int v) {
  if (dep[u] < dep[v]) swap(u, v);

  for (int i = lg[dep[u] - dep[v]]; i >= 0; --i) {
    if (dep[F[u][i]] > dep[v]) {
      u = F[u][i];
    }
  }

  if (fa[u] == v) return mk(u, u);

  if (dep[u] != dep[v]) u = fa[u];

  for (int i = lg[dep[u]]; i >= 0; --i) {
    if (F[u][i] != F[v][i]) {
      u = F[u][i];
      v = F[v][i];
    }
  }

  return mk(u, v);
}

int dist(int u, int v, int lca) { return dep[u] + dep[v] - 2 * dep[lca]; }

void getValue(int u) {
  for (int i = h[u]; i; i = e[i].nxt) {
    int v = e[i].v, w = e[i].w;
    if (v == fa[u] || w) continue;

    val[v] = val[u] + dp[u][1 << e[i].id];

    getValue(v);
  }
}

void chmax(int &x, int y) { x = max(x, y); }
void dfs2(int u) {
  val[u] = 0;

  for (int i = h[u]; i; i = e[i].nxt) {
    int v = e[i].v, w = e[i].w;
    if (v == fa[u] || w) continue;

    dfs2(v);
    val[v] = 0;
    getValue(v);
  }

  for (int S = 0; S < (1 << 10); S++) {
    for (int i = 0; i < 10; i++) {
      if ((S >> i & 1) == 0) dp[u][S] += dp[e[pos[u][i]].v][0];
    }
  }

  for (auto S : T) {
    for (auto j : _e[u]) {
      if ((S >> j.x & 1) || (S >> j.y & 1)) continue;

      if (j.x != j.y)
        chmax(dp[u][S], dp[u][S | (1 << j.x) | (1 << j.y)] + dp[j.u][0] +
                            dp[j.v][0] + val[j.u] + val[j.v] + j.w);
      else
        chmax(dp[u][S], dp[u][S | (1 << j.x) | (1 << j.y)] +
                            dp[dep[j.u] > dep[j.v] ? j.u : j.v][0] + j.w +
                            val[j.u] + val[j.v]);
    }
  }
}

signed main() {
#ifdef ZYC_
  freopen(".in", "r", stdin);
  freopen(".out", "w", stdout);
#endif
  //
  lg[0] = -1;
  for (int i = 2; i <= zyc - 10; i++) lg[i] = lg[i >> 1] + 1;
  for (int S = 0; S < (1 << 10); S++) {
    T[S] = S;
  }
  sort(T.begin(), T.end(), [](int x, int y) {
    return __builtin_popcount(x) > __builtin_popcount(y);
  });

  n = read(), m = read();
  int u, v, w, sumw = 0;

  for (int i = 1; i <= m; i++) {
    u = read(), v = read(), w = read();
    add_edge(u, v, w), add_edge(v, u, w);
    sumw += w;
  }

  dfs(1, 0);

  pre_lca();

  for (int i = 1; i <= n; i++) vis[i] = false;

  for (int u = 1; u <= n; u++) {
    for (int i = h[u]; i; i = e[i].nxt) {
      int v = e[i].v, w = e[i].w;

      if (vis[i] || !w) continue;

      vis[i] = vis[i ^ 1] = true;
      pii t = lca(u, v);
      int Lca = fa[t.first];

      if ((dep[u] + dep[v] - (dep[Lca] << 1) & 1) == 0) {
        _e[Lca].push_back(_Edge{u, v, e[edge[mk(Lca, t.first)]].id,
                                e[edge[mk(Lca, t.second)]].id, w});
      }
    }
  }

  dfs2(1);

  cout << sumw - dp[1][0];

  return 0;
}

J P4484 [BJWC2018] 最长上升子序列\(\ _{\text{By}\ \mathfrak{whrwlx}}\)

状压+打表

发现 \(n \le 28\),很 打表 的范围,但是如何 快速地暴力 是一个问题

事实上,从左向右推好像不是很可行,考虑对于一个排列,我们把数从小到大插入

那么我们首先令一个\(f_i\)(跟程序没关系)表示,在当前已经确定的一个序列里面,以第 \(i\) 个数结尾的最长上升子序列长度

基于这个数组,我们再令 \(maxL_i\) 表示 前缀最大值,即:

\[maxL_i = max\{f_1,f_2...,f_i\} \]

那么对于 \(maxL\) 数组,显然有:

\[maxL_i \leq maxL_{i+1} \leq maxL_{i} +1 \]

我们可以 差分 \(maxL\),不妨设对其进行差分的数组为 \(c\)

我们把数从小到大插入的时候,对于 \(c\) 数组:

考虑在第 \(i\) 位和第 \(i+1\) 位之间插入了一个新的数,因为是单调地插入的,所以新插入的这个数一定是当前序列的最大数

显然,这个数的 \(maxL\) 一定是 \(maxL_i+1\),因此把 \(c_{i+1}\) 改成 \(1\),而在 \(i\) 之后第一个比 \(a_i\) 大的数,记其位置为 \(pos\),则 \(c_{pos}\) 值肯定也为 \(1\)

但是当我们插入了这个新的数之后,由于在它刚刚插入的不计入 \(f_{pos}\),所以我们应当把 \(c_{pos}\) 置成 \(0\)

那么接下来要做的就是对 \(c\) 数组进行状压 DP

那我们不妨令 \(f_{i,j}\) 表示在一个 \(1\)~\(i\) 的排列里,差分数组 \(c\) 状态为 \(j\) 的方案数,那么答案就是:

\[ans=\frac{1}{n!}\sum_{i=0}^{2^{n-1}-1}{f_{n,i} \times len(i)} \]

也就是 \(\sum\) \((\)\(n\) 个数、状态为\(i\)的方案\(\times\)方案中的 LIS 的长度 \()\)

由于我们状压了 \(c\) 数组,所以每个方案中 LIS 的长度,就是该状态里 \(1\) 的个数

注意:

  • 序列 \(f\)\(c\) 仅用来推导

  • 记得开滚动数组

  • 由于一定存在 \(c_1=1\),实际有 \(n\) 个数的状态只需要记录最后 \(n-1\)

Code
#include<bits/stdc++.h>
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;

inline int read()
{
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
	return x*f;
}

const int N=3e5+5,M=1<<27|1,K=2e4+5,mod=998244353;
int n,m;
int *f[2]={new int[M],new int[M]},len[M],ans,fac;
int res[30]={0,1,499122178,2,915057326,540715694,946945688,422867403,451091574,317868537,200489273,976705134,705376344,662845575,331522185,228644314,262819964,686801362,495111839,947040129,414835038,696340671,749077581,301075008,314644758,102117126,819818153,273498600,267588741};

inline int qpow(int x,int y)
{
	int re=1;x%=mod;
	while(y)
	{
		if(y&1) (re*=x)%=mod;
		(x*=x)%=mod,y>>=1;
	}
	return re;
}
inline int inv(int x)
{
	return qpow(x,mod-2);
}

signed main()
{
// #define FJ
#ifdef FJ
	freopen("A.in","r",stdin);
	freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
#endif
#define Res
#ifdef Res
	//打表直接输出
	printf("%lld",res[read()]);
	return 0;
#endif
//#define Pre
#ifdef Pre
	//打表
	freopen("Pre.txt","w",stdout);
	int lim=28,T=0;
	DO:T=-~T;cerr<<"Now:"<<T<<endl;
	n=T-1,m=1<<n;
	fd(i,0,m) f[1][i]=f[0][i]=len[i]=0;
#else
	n=read()-1,m=1<<n;
#endif
	
	f[0][0]=1;
	fd(i,1,n)
	{
		int cur=i&1;//滚动数组
		fd(j,0,1<<i) f[cur][j]=0;//memset
		fd(j,0,(1<<(i-1))-1)
		{
			(f[cur][j<<1]+=f[cur^1][j])%=mod;
			int pos=-1;
			bd(k,i-1,0)//枚举插入到哪一位
			{
				int t=((j>>k)<<(k+1))|(1<<k)|(j&((1<<k)-1));
				//(j>>k)<<(k+1)是为了先清掉后面几位,不能简写成j<<1
				if(j&(1<<k)) pos=k;
				if(~pos) t^=(1<<(pos+1));
				(f[cur][t]+=f[cur^1][j])%=mod;
			}
		}
	}
	
	fd(i,1,m-1) len[i]=len[i-(i&-i)]+1;
	ans=0;fd(i,0,m-1) (ans+=f[n&1][i]*(len[i]+1))%=mod;
	fac=1;fd(i,1,n+1) (fac*=i)%=mod;
	
	ans=ans*inv(fac)%mod;
	printf("%lld,",ans);
	
#ifdef Pre
	if(T<lim) goto DO;
#endif
	
	return 0;
}

K UOJ #37. 【清华集训2014】主旋律\(\ _{\text{By}\ \mathfrak{whrwlx}}\)

容斥+状压(南通题)


简单一点的思路

题目等价于求有多少个边集可以使这张图强连通

即求这张图不强连通的方案数

\(Dag[s]\) 表示集合 \(s\) 是一个 \(DAG\),那么我们用全集减去它就是答案

我们再设 \(G[s]\) 表示集合 \(s\) 被划分为奇数个强连通分量的方案数,

\(H[s]\) 表示划分为偶数个强连通分量的方案数

\(E(S,T)\) 表示集合 \(S\) 向集合 \(T\) 连的边数

\[Dag[S]=\sum_{s \subset S}(G[S]-H[S])\times 2^{E(s,S-s)+E(S-s,S-s)} \]

最后加上自己连自己的方案数是因为我们的容斥系数已经弄好了,只需要让 \(S-s\) 缩完点之后成为一个 \(DAG\) 就行了,所以合法的边集是全集

我们最后的答案 \(f[s]\) 表示集合 \(s\) 强连通的方案数,\(G\)\(H\) 的转移有:

\[G[S]=\sum_{s \subset S}f[s]\times H[S-s]\\ H[S]=\sum_{s \subset S}f[s]\times G[S-s] \]

Code
#include "bits/stdc++.h"
#include "whrwlx/modint.h"
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;

inline int read()
{
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
	return x*f;
}

const int N=5+9+2,M=225,K=2e4+5,mod=1e9+7;

int n,m;
modint G[1<<N],H[1<<N],f[1<<N],pw[N*N];
bitset<M> in[1<<N],out[1<<N];

inline int calc(int x,int y)
{
	return (out[x]&in[y]).count();
}

signed main()
{
// #define FJ
#ifdef FJ
	freopen("A.in","r",stdin);
	freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
#endif

	n=read(),m=read();
	int MS=(1<<n)-1;
	
	pw[0]=1;
	fd(i,1,n*n) pw[i]=pw[i-1]*2;
	
	fd(i,1,m)
	{
		int x=read(),y=read();
		fd(j,1,MS)
		{
			if(j&(1<<(x-1))) out[j][i]=1;
			if(j&(1<<(y-1))) in[j][i]=1;
		}
	}
	
	H[0]=1;
	fd(S,1,MS)
	{
		f[S]=pw[calc(S,S)];
		for(int s=(S-1)&S;s;s=(s-1)&S)
		{
			f[S]=f[S]-(G[s]-H[s])*pw[calc(s,S-s)+calc(S-s,S-s)];
			if((s&(S&-S))==0) continue;
			G[S]+=f[s]*H[S-s];
			H[S]+=f[s]*G[S-s];
		}
		f[S]=f[S]-(G[S]-H[S]);
		G[S]+=f[S];
	}
	
	cout<<f[MS].num;
	
	return 0;
}

首先,根据正难则反的思路,合法方案数等于,总方案数 \(2^m\) 减去不合法方案数

不合法:即将图进行缩点之后并不是一个点而是一个 \(DAG\)

那么分为两部分:\(DAG\),以及其中的 \(SCC\)

首先定义 \(D(S)\) 为点集 \(S\) 构成的子图中,为 \(DAG\) 的子图个数

那么一个容斥是:

\[D(S) = \sum_{T \subseteq S} (-1)^{|T|+1}D(S\ \^{\ }\ T) \times 2^{E(T,S\ \^{\ }\ T)} \]

其中 \(E(S,T)\) 表示出点在点集 \(S\) 入点在点集 \(T\) 的边的数量

然后思考怎么算 SCC。首先定义一个 \(F(S)\) 表示点集 \(S\) 构成的子图中,为强连通图的子图个数(即答案)

接下来引入两个记号:

  • \(T \in \text{P}(S,k)\) 表示枚举将 \(S\) 拆分为 \(T_1,T_2,...,T_k\) 的所有方案,保证 $\forall i,j\in [1,k],T_i \cap T_j = \varnothing $
  • \(T \in \text{SP}(S,k)\) 表示枚举将 \(S\) 的严格拆分,即保证 \(k>1\)

那么可以列出 \(F\) 的公式:

\[F(S) = 2^{E(S,S)}-\sum_{T\in\text{SP}(S,k)} D(T)\times \prod_{i=1}^k F(T_i) \]

\(D(T)\) 展开,有:

\[F(S) = 2^{E(S,S)} - \sum_{T\in\text{SP}(S,k)} \left( \sum_{T'\subseteq T} (-1)^{|T'|+1} D(T\ \^{}\ T') \times 2^{E(T',T\ \^{}\ T')} \right) \times \prod_{i=1}^k F(T_i) \]

这个式子丝毫的不可做,但我们仔细观察这两个 \(\sum\) ,是枚举划分再枚举子集

我们反过来,先枚举子集,在枚举子集的划分,是完全等价的,那么有:

\[F(S)=2^{E(S,S)} - \sum_{T \subset S}\sum_{U\in\text{P}(T,k)}\sum_{V\in\text{P}((S\ \^{}\ T),l)} \left( (-1)^{k+1} D(V)\times 2^{E(T, S\ \^{}\ T)}\left(\prod _{i=1}^k F(U_i)\right)\times \left(\prod _{i=1}^l F(V_i)\right) \right) \]

考虑把 \((-1)^{k+1}\prod_{i=1}^k F(U_i)\) 提到第二个 \(\sum\) 后,发现这样其实可以使 \(U,V\) 两部分分开

设:

\[G(S)=\sum_{T\in\text{P}(S,k)} (-1)^{k+1} \prod_{i=1}^k F(T_i) \]

代入,得:

\[F(S)=2^{E(S,S)}-\sum_{T\ \^{}\ S}G(T)\times 2^{E(T,S\ \^{}\ T)} \left(\sum_{V\in\text{P}(S\ \^{}\ T,l)} D(V)\prod _{i=1}^l F(V_i)\right) \]

注意到 \(\sum_{V\in\text{P}(S\ \^{}\ T,l)} D(V)\prod _{i=1}^l F(V_i)\) 是枚举了 DAG 再将缩掉的点拆开,相当于任意图

所以原式等价于:

\[F(S)=2^{E(S,S)} - \sum_{T\ \^{}\ S}G(T)\times 2^{E(T,S\ \^{}\ T)+E(S\ \^{}\ T,S\ \^{}\ T)} \]

得到 \(G\) 的柿子:

\[G(S)=F(S)-\sum_{T \subset S,x\in T} F(T)G(S\ \^{}\ T) \]

其中 \(x\) 任取,因为如果没有 \(x\in T\) 的限制时,会算重

具体的,对于一张图,有一个 \(SCC\) 点集为 \(CCF\),在 \(T=CCF\)\(F(T)\) 被算了一次,在 \(CCF \subseteq S\ \^{}\ T\) 时又可能被算一遍

为什么 \(x \in T\) 可以让我们不重不漏?

  • 不重:
    考虑两张图相同,必要条件是 \(x\) 所在 \(SCC\) 的编号相同
    保证了编号不同,图自然就不重了

  • 不漏:
    首先 \(x\) 所在 \(SCC\) 会被枚举到,
    而其他部分在 \(G(S\ \^{}\ T)\) 已经确定是可以的了

最后 \(F,G\) 一起 DP 就行了

复杂度最优秀可以达到 \(O(3^n)\)

但是 \(n \le 15\),暴力求 \(E\) 也是可以的

复杂度 \(O(n3^n)\)

Code
#include<bits/stdc++.h>
#include "whrwlx/modint.h"
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;

inline int read()
{
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
	return x*f;
}

const int N=5+9+2,M=225,K=2e4+5,mod=1e9+7;

int n,m;
modint<int,mod> f[1<<N],g[1<<N],pw[N*N];
//SCC DAG pow_of_2
int e[N],rnk[1<<N];

#define count __builtin_popcount
inline int E(int x,int y)
{
	int re=0;
	for(;x;x-=(x&-x))
		re+=count(e[rnk[x&-x]]&y);
	return re;
}

signed main()
{
// #define FJ
#ifdef FJ
	freopen("A.in","r",stdin);
	freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
#endif
	
	n=read(),m=read();
	int MS=(1<<n)-1;
	
	pw[0]=1;
	fd(i,1,n*n) pw[i]=pw[i-1]*2;
	
	fd(i,1,m)
	{
		int x=read(),y=read();
		e[x-1]|=(1<<(y-1));
	}
	fd(i,0,n) rnk[1<<i]=i;
	
//	#define DB
	
	fd(S,1,MS)
	{
		if(count(S)==1)//只有一个点,没有子集
		{
			f[S]=1,g[S]=1;
			continue;
		}
		#ifdef DB
			bitset<20> D;
			D.set(S);
			cout<<S<<endl<<":["<<D<<"]"<<endl;
		#endif
		int x=(S&-S),s=S-x;//见上文 x 的含义
		for(int T=s;;T=(T-1)&s)
		{
			#ifdef DB
				bitset<20> D;
				D.set(T|x);
				cout<<"g["<<D<<"]"<<endl;
			#endif
			g[S]-=f[(T|x)]*g[S^(T|x)];
			//这里 f[S] 还没求出,得先减,之后再把剩的 f[S] 加回来
			if(!T) break;
		}
		for(int T=S;T;T=(T-1)&S)
		{
			#ifdef DB
				bitset<20> D;
				D.set(T);
				cout<<"f["<<D<<"]"<<endl;
			#endif
			f[S]+=g[T]*pw[E(S^T,S^T)]*pw[E(S^T,T)];
		}
		f[S]=pw[E(S,S)]-f[S];//减去 DAG 得到 SCC
		g[S]+=f[S];//加回来
		#ifdef DB
			cout<<endl;
		#endif
	}
	
	cout<<f[MS].num;
	
	return 0;
}
手搓的 modint.h
template<typename _T,_T _MOD>
struct modint
{
	_T Mod=_MOD,num;
	modint(_T _Mod=_MOD,_T _num=0)
	{
		Mod=_Mod,num=_num;
	}
	inline _T qpow(_T x,_T y)
	{
		_T re=1;x%=Mod;
		while(y)
		{
			if(y&1) (re*=x)%=Mod;
			(x*=x)%=Mod,y>>=1;
		}
		return re;
	}
	inline _T inv(_T x)
	{
		return qpow(x,Mod-2);
	}
	inline modint operator+(const modint& y)
	{
		return modint(Mod,num+y.num<Mod?num+y.num:num+y.num-Mod);
	}
	inline modint operator+(const _T& y)
	{
		return modint(Mod,num+y<Mod?num+y:num+y-Mod);
	}
	inline modint operator-(const modint& y)
	{
		return modint(Mod,num-y.num<0?num-y.num+Mod:num-y.num);
	}
	inline modint operator-(const _T& y)
	{
		return modint(Mod,num-y<0?num-y+Mod:num-y);
	}
	inline modint operator*(const modint& y)
	{
		return modint(Mod,1ll*num*y.num%Mod);
	}
	inline modint operator*(const _T& y)
	{
		return modint(Mod,1ll*num*y%Mod);
	}
	inline modint operator/(const modint& y)
	{
		return modint(Mod,1ll*num*inv(y.num)%Mod);
	}
	inline modint operator/(const _T& y)
	{
		return modint(Mod,1ll*num*inv(y)%Mod);
	}
	inline modint operator^(const _T& y)
	{
		return modint(Mod,qpow(num,y));
	}
	inline modint operator+=(const modint& y)
	{
		return (*this)=(*this)+y;
	}
	inline modint operator+=(const _T& y)
	{
		return (*this)=(*this)+y;
	}
	inline modint operator-=(const modint& y)
	{
		return (*this)=(*this)-y;
	}
	inline modint operator-=(const _T& y)
	{
		return (*this)=(*this)-y;
	}
	inline modint operator*=(const modint& y)
	{
		return (*this)=(*this)*y;
	}
	inline modint operator*=(const _T& y)
	{
		return (*this)=(*this)*y;
	}
	inline modint operator/=(const modint& y)
	{
		return (*this)=(*this)/y;
	}
	inline modint operator/=(const _T& y)
	{
		return (*this)=(*this)/y;
	}
	inline void operator=(const _T& y)
	{
		num=y;
	}
};

M [ABC306Ex] Balance Scale\(\ _{\text{By}\ \text{yxans}}\)

首先不考虑 \(=\) 的情况,将 \(a_i\)\(b_i\) 连边,问题等价于给边定向成 \(DAG\) 的方案数。

考虑对每次删除这个 \(DAG\) 的入度为 \(0\) 的点,每次删除的同时定向,最终就能够得到一组合法解。

于是我们记 \(f_S\) 表示 \(S\) 集合内的点都已被删除的方案数。

转移时枚举子集,如果这个子集是独立集,我们就可以转移,\(f_S \gets f_{S\ \^{}\ t}\)

但是这显然会有重复,因为独立集的子集肯定是独立集,于是我们转移的时候乘上容斥系数 \((-1)^{|t|+1}\) 即可。

再考虑有 \(=\) 的情况,这种情况下尽管子集不是独立集,我们也可以转移,因为我们可以把相邻的两点用 \(=\) 连接从而缩成一个点,记用 \(=\) 连接形成的连通块个数为 \(cnt\) ,这时的容斥系数是 \((-1)^{cnt+1}\) ,于是就做完了。复杂度是 \(O(2^nm+3^n)\) 的。

data
n = read(), m = read();
  for (int i = 1; i <= m; i++) u[i] = read(), v[i] = read();

  for (int i = 0; i < (1 << n); i++) {
    comp[i] = __builtin_popcount(i);
    for (int j = 1; j <= n; j++) fa[j] = j;
    for (int j = 1; j <= m; j++)
      if ((i & (1 << u[j] - 1)) && (i & (1 << v[j] - 1)))
        if (find(u[j]) != find(v[j])) fa[find(u[j])] = find(v[j]), comp[i]--;
  }
  f[0] = 1;
  for (int S = 1; S < (1 << n); S++) {
    for (int t = S; t; t = S & (t - 1)) {
      if (comp[t] & 1)
        f[S] += f[S ^ t];
      else
        f[S] += mod - f[S ^ t];
      f[S] = (f[S] + mod) % mod;
    }
  }

  cout << f[(1 << n) - 1];
posted @ 2024-10-29 15:03  whrwlx  阅读(81)  评论(0编辑  收藏  举报