61.[CEOI2020] 星际迷航
来自 动态规划题单。
当 \(D=0\) 时,我们思考怎么求出舰长是否必胜:
因为我们只能从 \(1\) 出发,所以如果我们以 \(1\) 为根,我们肯定只能一直往下走。
所以可以树形 DP,设 \(p_i\) 表示 \(i\) 点是否为先手必胜点 (下面简称为必胜点,必败点同理)。
根据 SG 函数的相关知识 \(p_i=mex_{j \in son(i)}\)。
最后就是看 \(p_1\) 是否为 true
。
当 \(D=1\) 时:
我们需要把第二棵树的一个点 \(y\) 接到第一棵树的一个点 \(x\) 下,所以如果我们选择走 \((x,y)\) 这条星门,那么走到第二棵树后就是以 \(y\) 为根了,所以我们还需要求出以每一个点为根时,\(p\) 数组的情况,我们先假设我们做 \(n\) 次树形 DP \(O(n^2)\) 地来求。
下面将以 \(p_{rt,i}\) 表示以 \(rt\) 为根时,\(i\) 是否为必胜点 (只有一棵树,即 \(D=0\))。
还是考虑树形 DP,设 \(g_i\) 表示在 \(D=1\) 时,以 \(1\) 为根,将第二棵树的一个点 \(y\) 拉过来当做 \(i\) 子树内一个点的儿子,使 \(i\) 成为必胜点(\(i\) 可以原来就是必胜点)的方案数。
首先如果 \(y\) 是一个必胜点,即 \(p_{y,y}=true\),那么把它接过来没有任何影响,所以我们在 \(dp\) 状态中认为 \(y\) 是一个必败点。(这里 \(y\) 假设给定,即计算方案数时不需要考虑 \(y\) 到底是第二棵树的哪个点)。
下面除了 \(y\) 节点,节点均指在原树中的 (即第一棵树)。
- 如果 \(i\) 原来有 \(\ge 2\) 个子节点 \(j\) 是必败点:
那么把 \(y\) 接过来不会有任何影响,因为 \(y\) 至多改变一个儿子的状态,\(g_i=size_i\)。 - 如果 \(i\) 原来有 \(1\) 个儿子 \(j\) 是必败点:
(1) 那么我要么不把 \(y\) 接在 \(j\) 的子树里,让他还是必败点,方案数 \(=size_i-size_j\)。
(2) 要么把 \(y\) 接在 \(j\) 的子树里,但是要使 \(j\) 还是必败点,方案数 \(=size_j-g_j\)。
综上 \(g_i=size_i-g_j\)。 - 如果 \(i\) 原来一个儿子都不是必败点:
那么我要使得让他的一个儿子变成必败点,\(g_i=1 + \sum (size_j - g_j) = size_i - \sum g_j\)(最开始 \(+1\) 是因为可以直接接在 \(i\) 下面)。
如果设 \(s_1\) 表示 \(p_{y,y}=true\) 的 \(y\) 的数量,\(s_0\) 表示 \(p_{y,y}=false\) 的 \(y\) 的数量,那么有:
\(ans = n\times s_1 \times p_{1,1} + g_1\times s_0\)。
意思是,如果以 \(1\) 为根时,\(1\) 点是必胜点(即 \(p_{1,1}=true\)),那么第二棵树里 \(s_1\) 中的点可以随便连,否则不能连。而 \(s_0\) 中的点只能连在可以使 \(1\) 点成为必胜点的那 \(g_1\) 个点中。
再次提醒:\(p_{rt,i},s_0,s_1\) 这些量都只是针对原树而言,并没有其他树连进来;\(g\) 只考虑的是 \(D=1\) 的情况。
时间复杂度 \(O(n^2)\)。
在下面的讨论中由于我们只用到了每一个 \(p_{rt,rt}\),所以将以 \(p_{rt}\) 代替 \(p_{rt,rt}\)。
当 \(D>1\) 时:
直觉告诉我们肯定是从后往前 DP,设 \(f_{i,0}\) 表示遍历到第 \(i\) 棵树,从第 \(i\) 棵树中选出一个必败点连向第 \(i-1\) 棵树(具体连第 \(i-1\) 棵树的哪个点未定)的方案数。
\(f_{i,1}\) 则是选出一个必胜点连向第 \(i-1\) 棵树。
由于我们每一次仅仅只需要挑出一个必胜点或必败点,这意味着我们需要求出以每一个 \(x\) 为根时,\(g_x\) 的值,因此下面的讨论将以 \(g_x\) 表示以 \(x\) 为根时,\(g_x\) 的值,而不是以 \(1\) 为根。
我们还是先暴力地做 \(n\) 次树形 DP 来得到这个东西。
- 如果第 \(i+1\) 棵树向第 \(i\) 棵树连进来的是必胜点,那么相当于没有连,所以从第 \(i\) 棵树选出一个必败点的方案为 \(s_0\),选出一个必胜点的方案为 \(s_1\),所以:
\(f_{i+1,1}\times n\times s_0 \to f_{i,0}\)
\(f_{i+1,1}\times n\times s_1 \to f_{i,1}\)
因为我们的 dp 状态里规定了:具体连第 \(i-1\) 棵树的哪个点未定,所以第 \(i+1\) 棵树先随便连一个点,再从那 \(s_0/s_1\) 个点中选出一个点连向 \(i-1\)。 - 如果第 \(i+1\) 棵树向第 \(i\) 棵树连进来的是必败点,那让一个点 \(x\) 成为必胜点的方案有 \(g_x\) 种,成为必败点的方案有 \(n-g_x\) 种,所以:
\(f_{i+1,0}\times \sum g_x \to f_{i,1}\)
\(f_{i+1,0}\times \sum (n-g_{x}) \to f_{i,0}\)
综上,得到转移方程:
\(f_{i,0}=f_{i+1,1}\times n\times s_0 + f_{i+1,0}\times \sum(n-g_x)\)
\(f_{i,1}=f_{i+1,1}\times n\times s_1 + f_{i+1,0}\times \sum g_x\)
容易预处理出,\(sum_1=\sum g_x,sum_0=\sum (n-g_x) = n^2 - sum_1\),所以转移是 \(O(1)\) 的。
边界:\(f_{D,0}=s_0,f_{D,1}=s_1\)。
答案:\(ans = n \times f_{1,1}\times p_1 + f_{1,0}\times g_1\)。
时间复杂度 \(O(n^2+D)\)。
设计完状态就是一些关于优化的套路了,我们来看要优化什么:
- 求 \(p_x\)。
- 求 \(g_x\)。
- 求 \(f_{i,0/1}\)。
\(1,2\) 直接换根 DP:
设 \(up_i\) 表示以 \(fa_i\) 为根时,去掉 \(i\) 这棵子树,\(p_{fa_i}\) 的值。
\(h_i\) 表示以 \(fa_i\) 为根时,去掉 \(i\) 这棵子树,\(g_{fa_i}\) 的值。
那么就可以用 \(i\) 号节点儿子的 \(p\) 和 \(up_i\) 更新 \(p_i\),儿子的 \(g_i\) 和 \(h_i\) 更新 \(g_i\),更新方法类似。
那怎么求 \(up\) 和 \(h\) 呢?
会发现 \(up_i\) 只需要用 \(up_{fa_i}\) 和 \(p_u\) ( \(u\) 是除了 \(i\) 以外 \(fa_i\) 的儿子) 类似更新。
\(h_i\) 只需要用 \(h_{fa_i}\) 和 \(g_u\) ( \(u\) 是除了 \(i\) 以外 \(fa_i\) 的儿子) 类似更新即可。
再来看 \(3\) 的转移:
\(f_{i,0}=f_{i+1,1}\times n\times s_0 + f_{i+1,0}\times sum_0\)。
\(f_{i,1}=f_{i+1,1}\times n\times s_1 + f_{i+1,0}\times sum_1\)。
这就非常的矩阵快速幂,因为只跟上一层的状态和 \(n,s_0,sum_0,s_1,sum_1\) 有关。
转移矩阵就不写了,手推即可。
DP 好题!点赞。
时间复杂度:\(O(n + \log D)\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,D;
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}int Size[N],fa[N],p[N],g[N];
vector<int> son[N];
vector<int> V[N]; //V[u] 保存满足 v 是 u 的儿子,且 p[v]=0 的 v
int Sumg[N]; //保存儿子的 g 的和
void dfs1(int u,int Fa){fa[u]=Fa;Size[u]=1;p[u]=0;int cnt=0,failson=0,sumg=0;for(int i=head[u];i;i=Next[i]){int v=to[i];if(v==fa[u]) continue;son[u].push_back(v);dfs1(v,u);Size[u]+=Size[v];if(p[v]==0) p[u]=1,cnt++,failson=v,V[u].push_back(v);(sumg+=g[v])%=mod;}Sumg[u]=sumg;if(cnt>=2) g[u]=Size[u];else if(cnt==1) g[u]=(Size[u]-g[failson]+mod)%mod;else g[u]=(Size[u]-sumg+mod)%mod;
}int up[N],h[N];
void dfs2(int u){//在计算 up 和 h 时不能每一次都遍历兄弟,不然碰到菊花就假了 if(fa[u]){up[u]=0;int cnt=0,failson_g=0,sumg=0;cnt=V[fa[u]].size();for(int v:V[fa[u]]) //这里遍历兄弟至多只遍历2个 if(v!=u){failson_g=g[v];break;} sumg=(Sumg[fa[u]]-g[u]+mod)%mod;if(p[u]==0) cnt--;if(fa[fa[u]]){if(up[fa[u]]==0) cnt++,failson_g=h[fa[u]];(sumg+=h[fa[u]])%=mod;}if(cnt) up[u]=1;if(cnt>=2) h[u]=n-Size[u]; //注意此时整棵树的大小要减去 Size[u] else if(cnt==1) h[u]=(n-Size[u]-failson_g+mod+mod)%mod;else h[u]=(n-Size[u]-sumg+mod+mod)%mod; }for(int i=head[u];i;i=Next[i]){int v=to[i];if(v==fa[u]) continue;dfs2(v);}
} int s0,s1,sum0,sum1;
void dfs3(int u){ //更新 p 和 g p[u]=0;int cnt=0,failson_g=0,sumg=0;for(int v:son[u]){if(p[v]==0) p[u]=1,cnt++,failson_g=g[v];(sumg+=g[v])%=mod;}if(fa[u]){if(up[u]==0) p[u]=1,cnt++,failson_g=h[u];(sumg+=h[u])%=mod;}if(cnt>=2) g[u]=n;else if(cnt==1) g[u]=(n-failson_g+mod)%mod;else g[u]=(n-sumg+mod)%mod;if(p[u]==0) s0++;else s1++;(sum1+=g[u])%=mod,(sum0+=(n-g[u]+mod)%mod)%=mod;for(int i=head[u];i;i=Next[i]){int v=to[i];if(v==fa[u]) continue;dfs3(v);}
}struct Matrix{int a[2][2];int n,m;void Init(){memset(a,0,sizeof a);}
}F,A;
Matrix operator * (const Matrix &A,const Matrix &B){Matrix C;C.Init();C.n=A.n,C.m=B.m;for(int k=0;k<=A.m;k++){for(int i=0;i<=C.n;i++){for(int j=0;j<=C.m;j++){(C.a[i][j]+=A.a[i][k]*B.a[k][j]%mod)%=mod;}}}return C;
}
Matrix Quick_power(Matrix A,int b){Matrix Ans;Ans.n=1,Ans.m=1;for(int i=0;i<=Ans.n;i++){ //单位矩阵 for(int j=0;j<=Ans.m;j++){if(i==j) Ans.a[i][j]=1;else Ans.a[i][j]=0;}}while(b){if(b&1) Ans=Ans*A;b>>=1,A=A*A;} return Ans;
}
signed main(){
// freopen("ball.in","r",stdin);
// freopen("ball.out","w",stdout);n=read(),D=read();for(int i=1;i<n;i++){int u=read(),v=read();add(u,v),add(v,u);}dfs1(1,0);dfs2(1);dfs3(1);F.n=0,F.m=1;F.a[0][0]=s0,F.a[0][1]=s1;A.n=1,A.m=1;A.a[0][0]=sum0;A.a[0][1]=sum1;A.a[1][0]=s0*n%mod;A.a[1][1]=s1*n%mod;F=F*Quick_power(A,D-1);printf("%lld\n", (F.a[0][1]*n%mod*p[1]+F.a[0][0]*g[1]%mod)%mod );return 0;
}
62.[NOI Online #1 入门组] 跑步
首先题目等价于正整数拆分。
\(f_{i,j}\):用前 \(i\) 个数构造的的不下降序列,和为 \(j\) 的方案数。
那么 \(f_{i,j}=f_{i-1,j}+f_{i,j-i}\),这就是完全背包
答案为。
$f_{n,n} $。
考虑根号分治,令 \(B=\sqrt n\):
- 对于 \(i<B\) 的部分我们仍然用上面的 dp
- 设 \(g_{i,j}\) 表示构造长度为 \(i\) 的不上升序列,每个数都 \(\ge B\),和为 \(j\) 的方案数。
那么 \(g_{i,j}=g_{i-1,j-m}+g_{i,j-i}\)。意思是每一次我要么在序列末尾加一个 \(B\),要么整体 \(+1\)。
由于 \(j\le n\),所以 \(i\le \sqrt n\)。
最后答案就枚举第一部分的和,\(ans=\sum_{j=0}^n (f_{B-1,j}\times \sum_{k=0}^{\frac{n}{B}}g_{k,n-j})\)。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,m,mod,f[N],g[320][N];
signed main(){n=read(),mod=read();m=sqrt(n);f[0]=1;for(int i=1;i<m;i++){for(int j=i;j<=n;j++){(f[j]+=f[j-i])%=mod;}}g[0][0]=1;for(int i=1;i<=n/m+1;i++){for(int j=m*i;j<=n;j++){g[i][j]=(long long)(g[i][j-i]+g[i-1][j-m])%mod;}}long long ans=0;for(int j=0;j<=n;j++){long long sum=0;for(int k=0;k<=n/m;k++) (sum+=(long long)g[k][n-j])%=mod;(ans+=(long long)(f[j]*sum)%mod)%=mod;}printf("%lld\n",ans);return 0;
}
63.CF1220E Tourism
这题流行随机化解法,又因为我以前没写过随机化算法,所以就作为第一道随机化题吧。
没有奇环,让我们想到黑白染色,所以我们随机给每个点随一个颜色。
每一次走的时候不能走相同颜色的边,求解时可以简单dp:
\(f_{i,j}\) 表示当前走了 \(i\) 条边,走到 \(j\) 的最短距离。
转移 \(O(k\times n^2)\) 显然。
根据时间限制,我们可以随机 \(4500\) 次。
这个做法错的可能只有一种,就是正确答案对应的路径上出现了颜色相同的相邻点,否则一定可以 dp 出正确的答案。
所以这 \(k\) 个点一共有 \(2^k\) 总染色情况,有且仅有 0101010...
或 1010101...
是对的,即一次正确的概率是:\(\frac{1}{(2^{k-1})}=\frac{1}{512}\),错误的概率是:\(\frac{511}{512}\)。
\(4500\) 次都不对的概率是 \((\frac{511}{512})^{4500} \approx 0.00015\),事实上可以随到 \(5000\) 次,那概率更低。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,k,a[85][85];
int col[85],f[15][85],ans=LLONG_MAX;
void work(){for(int i=1;i<=n;i++) col[i]=rand()%2;for(int i=0;i<=k;i++){for(int j=1;j<=n;j++){f[i][j]=0x3f3f3f3f3f3f3f3f;}}f[0][1]=0;for(int i=1;i<=k;i++){for(int j=1;j<=n;j++){for(int u=1;u<=n;u++){if(col[u]!=col[j])f[i][j]=min(f[i][j],f[i-1][u]+a[u][j]);}}}ans=min(ans,f[k][1]);
}
signed main(){srand(time(0));n=read(),k=read();for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){a[i][j]=read();}}int T=4500;while(T--) work();printf("%lld\n",ans);return 0;
}
64.采集
题目描述
有一棵 \(n\) 个点(\(n \le 10^4\))的树,每个点有一个 \([1,n]\) 的颜色,请找出点数最少的连通块,满足这个连通块有至少 \(k\) 种不同的颜色(\(k\le 5\))。
题解
因为 \(k\) 很小,考虑随机化乱搞。
首先给每个颜色随机映射到 \([1,k]\)。
如果最终的那 \(k\) 种颜色刚好被映射成了不同的 \(k\) 个
那么就正确,正确的概率是 \(\frac{k!}{k^k}\) ,随机 \(50\) 次即可。
随机完之后,考虑树形 dp 计算答案,设:
\(f_{i,s}\) 表示 \(i\) 这棵子树内选出集合 \(s\) 的颜色的最小连通块大小(一定要选 \(i\))。
转移每一次加进来一个子树更新即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,k;
int a[N];
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}int ans=INT_MAX,col[N];
int f[N][40];
void dfs(int u,int fa){f[u][1<<col[ a[u] ]]=1;for(int i=head[u];i;i=Next[i]){int v=to[i];if(v==fa) continue;dfs(v,u);for(int j=(1<<k)-1;j>=0;j--){for(int s=0;s<(1<<k);s++){f[u][j|s]=min(f[u][j|s],f[u][j]+f[v][s]);}}}ans=min(ans,f[u][(1<<k)-1]);
}
void work(){memset(f,0x3f,sizeof f);dfs(1,0);
}
signed main(){freopen("insect.in","r",stdin);freopen("insect.out","w",stdout);n=read(),k=read();bool flag=true;for(int i=1;i<=n;i++){a[i]=read();if(a[i]>k) flag=false;} for(int i=1;i<n;i++){int u=read(),v=read();add(u,v),add(v,u);}if(flag){for(int i=1;i<=k;i++) col[i]=i-1;work();printf("%d\n",ans);return 0; }mt19937 mtrand(time(0));int T=50;while(T--){for(int i=1;i<=n;i++){col[i]=(unsigned int)mtrand()%k;}work();}printf("%d\n",ans);return 0;
}
65.[ABC214G] Three Permutations
来自题解区的妙妙非传统 DP 做法。
首先容斥是显然的,设 \(dp_i\) 表示有 \(i\) 个位置不符合条件的数量,那么:
\(ans = \sum_{i=0}^n (-1)^i \times dp[i] \times (n-i)!\)。
考虑进行一个经典转化:
将 \(a_i\) 与 \(b_i\) 连边,那相当于我们要选出 \(i\) 条边,并给每条边定向,并且不能有两条边指向同一个点。
如果这 \(i\) 条边给定了,那就是一个有 \(n\) 个点, \(i\) 条边,每个点度数至多为 \(2\) 的图。
那一定是若干环加上若干链:
- 对于一个环,只有 \(2\) 中定向方案;
- 对于一条链,最终一定是选出一个点,它左边的边全朝左,右边的边全朝右。所以一共有 \(len\) 种方案,其中 \(len\) 为链的大小。
但是我们不能暴力枚举这 \(i\) 条边,所以考虑看到原图,即:\(n\) 个点,\(n\) 条边,每个点度数至多为 \(2\) 的图。这肯定是若干个环(有自环)组成的图。
设 \(f_{i,j}\) 表示前 \(i\) 个环,选出 \(j\) 条边定向的总方案数。
设第 \(i\) 个环的大小为 \(sz\)。
(目前为止都是主流做法,接下来就要开始神奇妙妙思路了)。
- 如果 \(sz=1\),即是自环,那么要么选,要么不选:\(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}\)。
- 如果 \(s\ne 1\),即正常的环,枚举这个环选了 \(k\) 条边,则:\(f_{i,j}=\sum_{k=0}^j f_{i-1,j-k}\times g_{sz,k}\)。其中 \(g_{sz,k}\) 表示在大小为 \(sz\) 的环中,选 \(k\) 条边定向的方案数。
这个做法妙的地方就是求 \(g\) 时不用 \(dp\) 用组合意义:可以把一条 \((u,v)\) 的边拆成 \((u,w)\) 和 \((w,v)\) ,定向朝 \(u\) 就是选 \((u,w)\) 这条边,否则选 \((w,v)\) 这条边。
那么问题变成,给你 \(2\times sz\) 个点的环,选出 \(k\) 条不相邻的边的方案。
随便考虑其中一条边 \((a,b)\):
- 如果不选他,问题就变成在 \(2\times sz\) 个点,\(2\times sz - 1\) 条边的链中选出 \(k\) 条不相邻边的方案。可以认为是先给你 \(2\times sz-k\) 个点,从中选出 \(k\) 个点作为右端点,再在这些点左边加上一个左端点。这样刚好一共 \(2\times sz\) 个点,且没有选出的边相邻,并且这样的结果也是可以唯一对应到原链的即方案为 \(C_{2\times sz-k}^k\)。
- 如果选他,那它旁边的边不能选了,问题就变成在 \(2\times sz-2\) 个点,\(2\times sz-3\) 条边的链中选出 \(k-1\) 条不相邻边的方案,方案为 \(C_{2\times sz-2-(k-1)}^{k-1}\)。
所以对于不是自环的 \(f\) 的转移为:\(f_{i,j} = f_{i-1,j} + \sum_{k=1}^j f_{i-1,j-k} \times ( C_{2\times sz-k}^k + C_{2\times sz-2-(k-1)}^{k-1})\)。
这个复杂度看似是 \(O(n^3)\) 但是其实是:
\(O(\sum_{i=1}^{cnt} s_i\times sz_i)\)。
其中 \(s\) 是 \(sz\) 的前缀和,\(cnt\) 是环的总数。
而:
所以时间复杂度是 \(O(n^2)\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e3+5,mod=1e9+7;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,a[N],b[N];
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){to[++tot]=v,Next[tot]=head[u],head[u]=tot;
} int fact[N<<1],inv[N<<1],q[N<<1];
void Init(){fact[0]=1;for(int i=1;i<N*2;i++) fact[i]=fact[i-1]*i%mod;inv[1]=1;for(int i=2;i<N*2;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;q[0]=1;for(int i=1;i<N*2;i++) q[i]=q[i-1]*inv[i]%mod;
}
int C(int n,int m){return fact[n]*q[m]%mod*q[n-m]%mod;
}int cnt,sz[N],s[N];
bool vis[N];
void dfs(int u,int len){sz[cnt]=max(sz[cnt],len);vis[u]=true;for(int i=head[u];i;i=Next[i]){int v=to[i];if(vis[v]) continue;dfs(v,len+1);}
}int f[N][N];signed main(){
// freopen("game.in","r",stdin);
// freopen("game.out","w",stdout);n=read();for(int i=1;i<=n;i++) a[i]=read();for(int i=1;i<=n;i++) b[i]=read(),add(a[i],b[i]),add(b[i],a[i]);Init();for(int i=1;i<=n;i++){if(!vis[i]){++cnt;dfs(i,1);s[cnt]=s[cnt-1]+sz[cnt];}}for(int i=0;i<=cnt;i++) f[i][0]=1;for(int i=1;i<=cnt;i++){for(int j=1;j<=s[i];j++){f[i][j]=f[i-1][j]; //下面就不要算不选的情况了 if(sz[i]==1) (f[i][j]+=f[i-1][j-1])%=mod;else{for(int k=1;k<=min(j,sz[i]);k++){(f[i][j]+=f[i-1][j-k] * ( C(2*sz[i]-k,k) + C(2*sz[i]-1-k,k-1) )%mod)%=mod;} }}} int ans=0;for(int i=0;i<=n;i++){if(i&1) (ans=ans-f[cnt][i]*fact[n-i]%mod+mod)%=mod;else (ans+=f[cnt][i]*fact[n-i]%mod)%=mod;}printf("%lld\n",ans);return 0;
}
66.[NOI Online #2 提高组] 游戏
二项式反演板子。
因为恰好 \(k\) 个不是很好算,所以考虑计算钦定 \(k\) 个的情况。
考虑树形 DP,设 \(f_{i,j}\) 表示 \(i\) 子树内,选出 \(j\) 对点,他们是非平局情况的方案数。
转移就是经典的树形背包,先转移不考虑根节点的情况,每一次合并进来一个子树:$ f_{u,x}\times f_{son,y} \to f_{u,x+y}\(。
再考虑选根节点的情况,\)f_{u,x}\times \max(cnt_{1-a_u}-x,0) \to f_{u,x+1}$ 其中 \(cnt_{0/1}\) 表示 \(u\) 子树内 \(0/1\) 类型的点的个数。
所以钦定 \(k\) 对点的方案数为 \(f_{1,k}\times (m-k)!\),如果设他是 \(g_k\),那么有:
\(g_k = \sum_{i=k}^m C_i^k \times ans_i\)。
二项式反演可得:
\(ans_k = \sum_{i=k}^m (-1)^{i-k} C_i^k \times g_i\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+5,mod=998244353;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,m,a[N];
vector<int> G[N];
int f[N][N],g[N],tmp[N],Size[N],cnt[N][2];
void dfs(int u,int fa){cnt[u][a[u]]++;f[u][0]=1;for(int v:G[u]){if(v==fa) continue;dfs(v,u);cnt[u][0]+=cnt[v][0];cnt[u][1]+=cnt[v][1];for(int i=0;i<=Size[u]+Size[v];i++) tmp[i]=0;for(int x=0;x<=Size[u];x++){for(int y=0;y<=Size[v];y++){(tmp[x+y]+=f[u][x]*f[v][y]%mod)%=mod;}}for(int i=0;i<=Size[u]+Size[v];i++) f[u][i]=tmp[i];Size[u]+=Size[v];}for(int i=Size[u];i>=0;i--) (f[u][i+1]+=f[u][i]*max(0ll,cnt[u][1-a[u]]-i)%mod)%=mod;Size[u]++;
}int fact[N],inv[N],q[N];
int C(int n,int m){return fact[n]*q[m]%mod*q[n-m]%mod;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);n=read(); m=n/2;string s; cin>>s;for(int i=1;i<=n;i++) a[i]=s[i-1]-'0';for(int i=1;i<n;i++){int u=read(),v=read();G[u].push_back(v);G[v].push_back(u);}dfs(1,0);fact[0]=1;for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;inv[1]=1;for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;q[0]=1;for(int i=1;i<N;i++) q[i]=q[i-1]*inv[i]%mod;for(int i=0;i<=m;i++) g[i]=f[1][i]*fact[m-i]%mod; for(int i=0;i<=m;i++){int ans=0;for(int j=i;j<=m;j++){ans=(ans+( ((j-i)%2==0)?1:-1 )*C(j,i)%mod*g[j]%mod+mod)%mod;}printf("%lld\n",ans);}return 0;
}
67.CF1895F Fancy Arrays
题目的存在这个要求很不友好,考虑计算不合法情况,他的否命题是:\(\forall i,ai<x 或 ai\ge x+k\)。
因为有任意相邻两数的差不超过 \(k\),所以上面这个条件等价于:每一个 \(a_i\) 都 \(<x\) 或者 每一个 \(a_i\) 都 \(\ge x+k\)。
还是不好算?
再一次看他的否命题,就得到了原命题的等价命题:$ \max(a_i)\ge x,\min(a_i)\le x+k-1$。当然还要满足 \(\forall i \in [2,n],|a_i-a_{i-1}|\le k\),下面就统一省略后面这个条件了。
因为 \(\max(a_i)<x\) 是 \(\min(a_i)\le x+k-1\) 的充分条件,所以可以转化为求:
\(\min(a_i)\le x+k-1 的数列数 - \max(a_i) < x 的数列数\)。
先看前者:
如果知道了一个序列的差分数组就可以知道序列的每一个数与 \(a_1\) 的差。
此时就可以知道哪个数是最小值,也就是说差分数组和最小值可以唯一确定一个序列。
差分数组的方案数是 \((2\times k+1)^{n-1}\),最小值的方案数是 \(x+k\) ( \(0\) 也算),所以总方案数就是 \((x+k) \times (2*k+1)^{n-1}\)。
再看后者,因为 \(x\) 很小,考虑 DP:
设 \(f_{i,j}\) 表示前 \(i\) 个数,第 \(i\) 个数是 \(j\) 的方案数,那么:
矩阵快速幂优化 DP 即可。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int T;
int n,x,k;
int quick_power(int a,int b){int ans=1;while(b){if(b&1) (ans*=a)%=mod;(a*=a)%=mod,b>>=1;}return ans;
}
struct Matrix{int a[45][45];int n,m;void Init(){memset(a,0,sizeof a);}
}A,F;
Matrix operator * (const Matrix &A,const Matrix &B){Matrix C;C.Init();C.n=A.n,C.m=B.m;for(int k=0;k<A.m;k++){for(int i=0;i<C.n;i++){for(int j=0;j<C.m;j++){(C.a[i][j]+=A.a[i][k]*B.a[k][j]%mod)%=mod;}}}return C;
}
Matrix Quick_power(Matrix A,int b){Matrix Ans;Ans.n=x,Ans.m=x;for(int i=0;i<Ans.n;i++){ //单位矩阵 for(int j=0;j<Ans.m;j++){if(i==j) Ans.a[i][j]=1;else Ans.a[i][j]=0;}}while(b){if(b&1) Ans=Ans*A;b>>=1,A=A*A;} return Ans;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);T=read();while(T--){n=read(),x=read(),k=read();int ans=(x+k)*quick_power(2*k+1,n-1)%mod;A.Init(),F.Init();A.n=x,A.m=x;for(int i=0;i<x;i++){for(int j=0;j<x;j++){if(abs(j-i)<=k) A.a[i][j]=1;}}F.n=1,F.m=x;for(int i=0;i<x;i++) F.a[0][i]=1;A=Quick_power(A,n-1);F=F*A;for(int i=0;i<x;i++)ans=(ans-F.a[0][i]+mod)%mod;printf("%lld\n",ans);}return 0;
}
68.[ARC162E] Strange Constraints
关于这种填数的题有一个经典套路是按照值域从小到大或从大到小填数。
但是这里不好做,因为虽然第二个限制条件很好满足,但是第三个限制条件由于我们不知道目前有哪些位置是可以填的所以不好转移。
所以设计的状态需要满足对于第二个限制条件,前面的状态要能限制到后面的状态并且可以比较容易得出哪些位置可以填。
所以我们按照每个数在 \(b\) 中出现的次数从大到小枚举。
设 \(f_{i,j,k}\) 表示当前枚举到的次数为 \(i\),填完之后有 \(j\) 个数已经被填过了,有 \(k\) 个位置已经被填过了。
转移时枚举有 \(x\) 个数被填了 \(i\) 次,所以是从 \(f_{i+1,j-x,k-i\times x}\) 转移过来,转移系数有下面这几个,我们分别做解释,设 \(cnt_{y}\) 表示 \(A\) 中大于等于 \(y\) 的数的个数:
- \(C_{cnt_i-(j-x)}^x\):
这是在从满足条件二的数中选 \(x\) 个数出来,因为 \(i\) 是从大到小枚举所以 \(cnt_i\) 也包含了$ cnt_{i+1},cnt_{i+2},...$ 那些位置,即前面的 \(j-x\) 个数也是从这 \(cnt_i\) 个数里选出来的,不能再选了。 - \(C_{cnt_i-(k-i\times x)}^{i\times x}\):
这是在给这 \(x\) 个数选位置,同理前面的 \(k-i\times x\) 个位置也是从 \(cnt[i]\) 个位置里选出来的,不能再选了。这也是这么设计状态的原因,这就满足了条件三。 - \(\frac{(i\times x)!} {(i!)^x}\):
这是可重集排列数。
这个 DP 貌似是 \(O(n^4)\) 但是容易发现 \(j\) 和 \(x\) 的枚举范围分别是 \(\frac{n}{i}\) (因为 \(i\) 从大到小枚举) 和 \(\frac{k}{i}\)。
所以时间复杂度为:
而 \(∑\frac{1}{i^2}\) 可以省略,即时间复杂度是:\(O(n^3)\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=500+5,mod=998244353;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,a[505],cnt[505],f[505][505][505];
int fact[N],inv[N],pre[N];
void Init(){fact[0]=1;for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;inv[1]=1;for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;pre[0]=1;for(int i=1;i<N;i++) pre[i]=inv[i]*pre[i-1]%mod;
}
int C(int n,int m){if(m>n) return 0;return fact[n]*pre[m]%mod*pre[n-m]%mod;
}
int quick_power(int a,int b){int ans=1;while(b){if(b&1) (ans*=a)%=mod;(a*=a)%=mod,b>>=1; }return ans;
}
signed main(){n=read();for(int i=1;i<=n;i++) a[i]=read(),cnt[a[i]]++;for(int i=n;i>=1;i--) cnt[i]+=cnt[i+1];Init();f[n+1][0][0]=1;for(int i=n;i>=1;i--){for(int j=0;j<=n/i;j++){for(int k=0;k<=n;k++){for(int x=0;x<=min(j,k/i);x++){(f[i][j][k] += f[i+1][j-x][k-i*x]* C(cnt[i]-(j-x) , x) % mod* C(cnt[i]-(k-i*x) , i*x) % mod* fact[i*x]%mod * quick_power(pre[i],x)%mod) %= mod;}}}}int ans=0;for(int i=0;i<=n;i++) (ans+=f[1][i][n])%=mod;printf("%lld\n",ans);return 0;
}
69.[ARC163D] Sum of SCC
这题主要是要知道一个结论来把 SCC 这个一看就不好求得东西转换一下。
结论:竞赛图的 SCC 的个数等于把这张图的点集 \(V\) 划分成两个集合(可以为空) \(A\) 和 \(B\) 且 \(A\)
与 \(B\) 之间的边方向都为 \(A\to B\) 的划分方案数 \(-1\)。
证明:
考虑缩点,缩点之后的竞赛图也是一个形似链的竞赛图,他的拓扑序是唯一的。
如果拓扑序为 \(p_1,p_2,...,p_k\)(\(k\) 为 SCC 的个数),那么对于任意的一个 \(i(0\le i\le k)\) 我们以 \(i\) 为断点,\(p_1,p_2,...,p_i\) 放进 \(A\),\(p_{i+1},...,p_k\) 放进 \(B\) 就是一个合法的划分方案。
又因为不能把一个 SCC 中的点分到两个集合中(如果可以的话这必然不是个 SCC,因为划分到 \(B\) 中的点没有到 \(A\) 中的点的路径),\(B\) 中的 SCC 的拓扑序也不能比 \(A\) 中的 SCC 的拓扑序小,所以这种划分方案也是唯一的。
一共有 \(k+1\) 种划分方案。
接下来就是简单 dp 了,设 \(f_{i,j,k}\) 表示已经放了前 \(i+j\) 个点,其中 \(|A|=i\),\(|B|=j\),并且一共有 \(k\) 条边满足 \(u<v\) 的方案数。
转移时考虑新加进来编号为 \(i+j+1\) 的点放到哪个集合:
- 放到 \(A\) 集合,那么因为他是目前编号最大的点所以他连向 \(B\) 中的那 \(j\) 条边均不符合条件,而他连向 \(A\) 中的那 \(i\) 条边是随便钦定的,枚举其中有 \(x\) 条边满足条件,转移为:\(f_{i,j,k}\times C_i^x \to f_{i+1,j,k+x}\)。
- 放到 \(B\) 集合,所以 \(A\) 中连向他的那 \(i\) 条边均符合条件,而他连向 \(B\) 中的那 \(j\) 条边是随便钦定的,枚举其中有 \(x\) 条边满足条件,转移为:\(f_{i,j,k}\times C_j^x \to f_{i,j+1,k+i+x}\)。
统计答案时,任意的一个方案都会给答案带来 \(1\) 的贡献,不用去管哪些方案是一张图的。
记得还要减去若干个 \(1\)。
时间复杂度 \(O(n^3 \times m)\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,m,f[35][35][905],ans;
int fact[N],inv[N],pre[N];
void Init(){fact[0]=1;for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;inv[1]=1;for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;pre[0]=1;for(int i=1;i<N;i++) pre[i]=inv[i]*pre[i-1]%mod;
}
int C(int n,int m){return fact[n]*pre[m]%mod*pre[n-m]%mod;
}
int quick_power(int a,int b){int ans=1;while(b){if(b&1) (ans*=a)%=mod;(a*=a)%=mod,b>>=1; }return ans;
}
signed main(){Init();n=read(),m=read();f[0][0][0]=1;for(int i=0;i<=n;i++){for(int j=0;j+i<=n;j++){for(int k=0;k<=m;k++){for(int x=0;x<=i;x++)(f[i+1][j][k+x]+=f[i][j][k]*C(i,x)%mod)%=mod;for(int x=0;x<=j;x++)(f[i][j+1][k+i+x]+=f[i][j][k]*C(j,x)%mod)%=mod; }}}for(int i=0;i<=n;i++){(ans+=f[i][n-i][m])%=mod;}(ans=ans-C(n*(n-1ll)/2ll,m)+mod)%=mod;printf("%lld\n",ans);return 0;
}
70.CF1728G Illumination
考虑容斥,枚举集合 \(S\) 表示 \(S\) 中的这些点不被照亮,其他点随意,则 \(ans=\sum(-1)^{|S|} \times f_S\)。
其中 \(f_S\) 表示对应的方案数。
首先每个不被照亮的点会对他周围的灯的照亮范围产生限制,并且两个钦定点之间的路灯仅仅只受到这两个点的限制。
这启发我们预处理 \(g_{l,r}\) 表示使得点 \(p_l\) 和 \(p_r\) 之间的路灯照不到 \(p_l\) 和 \(p_r\) 的方案数。
\(O(nm^2)\) 预处理即可。
那么 \(f_S= \prod g_{S_i,S_i+1}\)。
注意到直接这么算是不对的,因为会漏了两侧的点,解决方法是加上 \(-\infty\) 和 \(+\infty\) 两个点,并且钦定 \(S\) 中一定要包含这两个点(容易发现此时不影响 \(|S|\) 的奇偶性)。
由此我们可以 \(O(m2^m)\) 处理单个问题。
这里开始会出现两种做法,一种是算出每个 \(g_{l,r}\) 的容斥系数,动态加入一盏灯时算出他所影响的 \(g\) 的贡献。
但是比较麻烦且复杂度不优,这里讲第二种。
经典套路之——容斥转 DP。
设 \(dp_i\) 表示只考虑前 \(i\) 个点的答案,并且钦定第 \(i\) 个点一定在容斥的集合 \(S\) 里。
因为多加进来了一个点,所以原先容斥系数正的会变成负的,负的会变成正的,即转移要带上容斥系数 \(-1\)。
\(dp_i=\sum_{\substack{j<i}} (-1)\times dp_j\times g_{j,i}\)。
初始状态时 \(dp[0]=-1\)。
由这个转移式子可以看出 \(0\) 号点(无穷小) 是一定会被选的。
答案就是 \(dp_{m+1}\) 因为 \(m+1\) 号点也一定要被选。
单次 dp 复杂度 \(O(m^2)\),每一次都做一遍就是 \(O(Tn^2)\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5,mod=998244353,inf=INT_MAX;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int d,n,m,a[N],p[N],T;
int g[20][20],tmp[20][20],f[20];
signed main(){d=read(),n=read(),m=read();for(int i=1;i<=n;i++) a[i]=read();for(int i=1;i<=m;i++) p[i]=read();sort(p+1,p+m+1);p[0]=-inf,p[m+1]=inf;for(int l=0;l<=m+1;l++)for(int r=l+1;r<=m+1;r++)g[l][r]=1;for(int i=1;i<=n;i++)for(int l=0;l<=m+1;l++)for(int r=l+1;r<=m+1;r++)if(p[l]<=a[i]&&a[i]<=p[r])(g[l][r]*=min({d+1ll,a[i]-p[l],p[r]-a[i]}))%=mod; for(int l=0;l<=m+1;l++)for(int r=l+1;r<=m+1;r++){tmp[l][r]=g[l][r];}T=read();while(T--){int x=read();for(int l=0;l<=m+1;l++)for(int r=l+1;r<=m+1;r++)if(p[l]<=x&&x<=p[r])(g[l][r]*=min({d+1,x-p[l],p[r]-x}))%=mod; for(int i=0;i<=m+1;i++) f[i]=0;f[0]=-1;for(int i=1;i<=m+1;i++){for(int j=0;j<i;j++)(f[i]=f[i]-f[j]*g[j][i]%mod+mod)%=mod;}printf("%lld\n",f[m+1]);for(int l=0;l<=m+1;l++)for(int r=l+1;r<=m+1;r++)g[l][r]=tmp[l][r]; }return 0;
}
71.[ARC162D] Smallest Vertices
注意 \(d_i\) 的定义是儿子的个数。
前置知识:Prufer序列,具体看 OI.wiki。
涉及结论:
对于给定每个点度数的所有本质不同的 \(n\) 个点的有标号无根树一共有( \(deg_i\)表示 \(i\) 的度数):
\(\frac{(n-2)!}{\prod(deg_i-1)!}\)。
证明:
根据 Prufer 序列的性质,每个点在序列中出现的次数是 \(deg_i-1\),所以我们可以根据度数还原出 Prufer 序列里面的元素种类,总共有 \((n-2)!\) 种顺序,去掉重复的即为上述结果。
对于每一个点考虑计算他为好点的有根树的数量,即拆分贡献。
对于一个点 \(u\) 如果他是好点意味着它子树内的点在 \([u,n]\) 范围内。
枚举它的子树大小 \(sz\),且根据 Prufer 序列他们的 \(\sum (deg_i-1)=sz-2\)。
注意到此时我们认为 \(u\) 是根,所以他的 \(deg_u=d_u\),其余点的 \(deg_v=d_v+1\)。
所以上面式子的要求即为 \(\sum_{v∈subtree(u)} d_v = sz-1\)。
容易想到背包,设 \(f_{i,j,k}\) 表示从 \([i,n]\) 中选出 \(j\) 个点,他们的 \(d\) 的和为 \(k\) 的方案数。
转移略。
那么对于 \(u\) 子树内的贡献就是:
对于 \(u\) 子树外的贡献,此时我们把 \(u\) 当做一个叶子结点,并且他子树内的那 \(sz-1\) 个点删掉再计算,
注意 \(1\) 的度数也是 \(d_1\),而不是 \(d_1+1\),贡献为:\(\frac{ (n-sz+1-2)! \times d_1}{ \prod d_v! }\) ,其中 \(v\) 不属于 \(u\) 的子树(\(u\) 此时为叶子的话他的度数 \(-1=0\),可以不考虑进去)。
把上面这两个东西乘起来会发现分母刚好等于 \(\prod_{i=1}^n d_i!\) ,可以直接预处理。
于是最后的贡献就是:
这里写 $ f_{u+1,sz-1,sz-1-d_u}$ 而不是 \(f_{u,sz,sz-1}\) 是因为不能出现不选 \(u\) 的情况。
枚举 \(u\) 和 \(sz\) 计算答案即可。
注意这里如果 \(u\) 是根节点或者是叶子结点要特判一下 (因为 \(sz\ge 2\),且 \(u\) 子树外不能为空) ,此时贡献就是所有符合题意的无根树的数量。
总时间复杂度 \(O(n^3)-O(n^2)\)(这个的意思是预处理 \(O(n^3)\),求答案 \(O(n^2)\))。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,d[N],f[505][505][505],ans;
int fact[N],inv[N],pre[N];
void Init(){fact[0]=1;for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;inv[1]=1;for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;pre[0]=1;for(int i=1;i<N;i++) pre[i]=inv[i]*pre[i-1]%mod;
}
signed main(){Init(); n=read();for(int i=1;i<=n;i++) d[i]=read();f[n+1][0][0]=1;for(int i=n;i>=1;i--){for(int j=0;j<=n;j++){for(int k=0;k<=n-1;k++){f[i][j][k]=f[i+1][j][k];if(j>0&&k>=d[i]) f[i][j][k]=(f[i][j][k]+f[i+1][j-1][k-d[i]])%mod;}}} int tmp=1;for(int i=1;i<=n;i++) (tmp*=pre[d[i]])%=mod; //预处理分母for(int u=1;u<=n;u++){if(u==1||d[u]==0)(ans += d[1] * fact[n-2] % mod * tmp % mod) %= mod;elsefor(int sz=2;sz<n;sz++){(ans += d[1] * fact[n-sz-1] % mod * d[u] % mod * fact[sz-2] % mod * f[u+1][sz-1][sz-1-d[u]] % mod * tmp % mod) %= mod;}}printf("%lld\n",ans);return 0;
}
72.[ABC306Ex] Balance Scale
根据题目意思可以连边,如果没有 =
相当于给每条边定向。
因为不能出现环,所以相当于一个有标号 DAG 计数。
经典思路设 \(f_S\) 表示 \(S\) 中的点的导出子图一共有多少种可能的 DAG。
因为一个 DAG 必然有一些入度为 \(0\) 的点,考虑容斥这些点,去掉这些点仍然是个 DAG 那么:
\(\sum_{T \in S} (-1)^{|T|} \times f_{S-T}\),\(S-T\) 表示的是补集
这个东西算出来的结果表示的意义是 \(S\) 中的点形成的 DAG 中有 \(0\) 个入度为 \(0\) 的点的方案数。
所以神奇的地方来了,因为这种 DAG 不存在,所以他等于 \(0\)。
那么移项即可得到:
\(f_S=\sum_{T \in S,T\ne \emptyset}(-1)^{|T|+1} \times f_{S-T}\)。
注意 \(T\) 中的点之间不能有连边。
加上 =
的情况也很简单,其实相当于可以合并这条边的两个端点所以 \(T\) 中的点就可以出现有连边的情况,容斥系数从 \((-1)^{|T|+1}\) 变成 \((-1)^{cnt_T + 1}\) 即可,\(cnt_T\) 为 \(T\) 中的点的导出子图的连通块数量。
时间复杂度 \(O(3^n)\)。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}
int n,m;
bool G[20][20];
int fa[20];
int get(int x){return (x==fa[x])?(x):(fa[x]=get(fa[x]));
}
void merge(int x,int y){fa[get(x)]=get(y);
}
int cnt[(1<<17)+5]; //预处理连通块个数
int f[(1<<17)+5];
signed main(){n=read(),m=read();for(int i=1;i<=m;i++){int a=read(),b=read();G[a][b]=G[b][a]=true;}for(int s=0;s<(1<<n);s++){for(int i=1;i<=n;i++) fa[i]=i;for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){if(G[i][j]&&(s>>(i-1)&1)&&(s>>(j-1)&1))if(get(i)!=get(j)) merge(i,j);}}for(int i=1;i<=n;i++)if((s>>(i-1)&1)&&get(i)==i) cnt[s]++;}f[0]=1;for(int s=1;s<(1<<n);s++){for(int t=s;t;t=(t-1)&s){if((cnt[t]+1)&1) f[s]=(f[s]+mod-f[s^t])%mod;else f[s]=(f[s]+f[s^t])%mod;} }printf("%lld\n",f[(1<<n)-1]);return 0;
}