[AGC043C] Giant Graph 题解

发布时间 2023-12-19 19:26:30作者: 小超手123

题意:

给定三个简单无向图\(G_1,G_2,G_3\),其中每个图的点数均为\(n\),边数分别为\(m_1,m_2,m_3\)

现在根据\(G_1,G_2,G_3\)构造一个新的无向图\(G\)\(G\)\(n^3\)个点,每个点可以表示为\((x,y,z)\),对应\(G_1\)中的点\(x\)\(G_2\)中的点\(y\)\(G_3\)中的点\(z\)。边集的构造方式如下:

\(G_1\)中存在一条边\((u,v)\),则对于任意\(1\le a, b\le n\),在\(G\)中添加边\(((u,a,b),(v,a,b))\)

\(G_2\)中存在一条边\((u,v)\),则对于任意\(1\le a, b\le n\),在\(G\)中添加边\(((a,u,b),(a,v,b))\)

\(G_3\)中存在一条边\((u,v)\),则对于任意\(1\le a, b\le n\),在\(G\)中添加边\(((a,b,u),(a,b,v))\).

对于\(G\)中的任意一个点\((x,y,z)\),定义其点权为\(10^{18(x+y+z)}\)

试求\(G\)的最大权独立集的大小模\(998244353\)的值。

\(2\le n\le 10^5, 1\le m_1,m_2,m_3\le 10^5\)

分析:

由于点权为 \(10^{18(x+y+z)}\),显然求最大权独立集时可以直接贪心,因为取编号大的一定更优。

考虑只有 \(G_1\) 的情况,我们首先把 \(G_1\) 中的边定向(编号小的连到编号大的),然后可以发现,独立集的定义可以转化成一个经典的博弈问题:给定一张 DAG,两个人轮流操作,最先无法操作的人失败。

因此可以使用 SG 函数解决。对重构图中出度为 \(0\) 的点定义其 \(SG(x)=0\),然后转移 \(SG(x)=mex(SG(y))\)

这样做后可以发现 \(SG\) 值为 \(0\) 的点一定会取,因为根据 \(mex\) 的定义,即存在至少一个 \(SG(y)\)\(0\),那么 \(SG(x)\) 一定不为 \(0\)。这里先手必败就一定会取。

那这里三个图怎么做呢?可以发现每个点的三个维度的值的变换是独立的。

根据 \(SG\) 定理:

对于由 \(n\)个有向图游戏组成的组合游戏,设它们的起点分别为 \(s_1,s_2,...,s_{n}\),则有定理:当且仅当 \(SG(s_1) \oplus SG(s_2) \oplus ... \oplus SG(s_n) \ne 0\) 时,这个游戏是先手必胜的。

因此我们需要统计:

\[\sum_{x_1=1}^{n} \sum_{x_2=1}^{n}\sum_{x_3=1}^{n} V^{x_1+x_2+x_3}[SG(x_1) \oplus SG(x_2) \oplus SG(x_3)==0] \]

这东西就很好做了,记 \(f_{i,j}\) 表示目前处理到 \(x_i\),异或和为 \(j\) 的所有方案的权值和

转移显然为 f[i + 1][j ^ a[i].SG[g]] += f[i][j] * h[g]

暴力转移是 \(O(n^2)\) 的。但众所周知,一个 DAG 的 \(SG\) 函数的值域在 \([0,\sqrt m]\)。因此可以做到 \(O(m)\)\(O(n \sqrt m)\) 的时间复杂度,前者需要搞个 \(SG\) 的桶。

代码:

#include<bits/stdc++.h>
#define int long long
#define mod 998244353
#define N 100005
using namespace std;
int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
	return x * f;
}
void write(int x) {
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) write(x / 10);
	putchar('0' + x % 10);
}
int Pow(int a, int n) {
	if(n == 0) return 1;
	if(n == 1) return a;
	int x = Pow(a, n / 2);
	if(n % 2 == 0) return x * x % mod;
	else return x * x % mod * a % mod;
}
int n, k, v, h[N];
int z[N], f[7][N]; //f[i][j]表示考虑前i行,异或和为j的权值和 
struct Graph{
	vector<int>G[N], p[N];
	int outd[N], SG[N], t[N]; //t[i]是SG函数的桶 
	void work(int x) {
		for(auto y : p[x]) z[SG[y]] = 1;
		SG[x] = 0; while(z[SG[x]]) SG[x]++;
		for(auto y : p[x]) z[SG[y]] = 0;
	}
	void GetSG() { //拓扑排序求SG函数 
		queue<int>Q;
		for(int i = 1; i <= n; i++) 
			if(outd[i] == 0) Q.push(i);
		while(!Q.empty()) {
			int x = Q.front();
			Q.pop();
			for(auto y : G[x]) {
				outd[y]--;
				if(outd[y] == 0) work(y), Q.push(y);
			}
		}
		for(int i = 1; i <= n; i++) t[SG[i]] += i;
	}
}a[7];
signed main() {
    n = read(); 
	k = 3;
    v = Pow(10, 18) % mod;
    h[0] = 1; for(int i = 1; i <= n; i++) h[i] = h[i - 1] * v % mod;
    for(int i = 1; i <= k; i++) {
    	int mi = read();
    	while(mi--) {
    		int u = read(), v = read();
    		if(u > v) swap(u, v); //小的往大的连边 
    		a[i].G[v].push_back(u); //连反向边
    		a[i].p[u].push_back(v); //连正向边
			a[i].outd[u]++; 
	    }
	}
	for(int i = 1; i <= k; i++) a[i].GetSG(); 
	f[1][0] = 1;
	for(int i = 1; i <= k; i++) 
		for(int j = 0; j <= 700; j++) 
			for(int g = 1; g <= n; g++) {
				f[i + 1][j ^ a[i].SG[g]] += f[i][j] * h[g];
				f[i + 1][j ^ a[i].SG[g]] %= mod;
		    }
	write(f[k + 1][0]);
	return 0;
}