@loj - 2496@ 「AHOI / HNOI2018」毒瘤
@description@
从前有一名毒瘤。
毒瘤最近发现了量产毒瘤题的奥秘。考虑如下类型的数据结构题:给出一个数组,要求支持若干种奇奇怪怪的修改操作(例如给一个区间内的数同时加上 c,或者将一个区间内的数同时开平方根),并且支持询问区间的和。毒瘤考虑了 n 个这样的修改操作,并将它们编号为 1...n。当毒瘤要出数据结构题的时候,他就将这些修改操作中选若干个出来,然后出成一道题。
当然了,这样出的题有可能不可做。通过精妙的数学推理,毒瘤揭露了这些修改操作之间的关系:有
m 对「互相排斥」的修改操作,第 i 对是第 ui 操作和第 vi 个操作。当一道题中同时含有 ui 和 vi 这两个操作时,这道题就会变得不可做。另一方面,当一道题中不包含任何「互相排斥」的操作时,这个题就是可做的。
此外,毒瘤还发现了一个规律: m - n 是一个很小的数字(参见「数据范围」中的说明),且任意两个修改操作都是连通的。两个修改操作 a, b 是连通的,当且仅当存在若干操作 t0, t1, ..., tl,使得 t0 = a, tl = b,且对任意 1 <= i <= l,t[i] 与 t[i-1] 都是「互相排斥」的修改操作。
一对「互相排斥」的修改操作称为互斥对。现在毒瘤想知道,给定值 n 和 m 个互斥对,他一共能出出多少道可做的不同的数据结构题。两个数据结构题是不同的,当且仅当其中某个操作出现在了其中一个题中,但是没有出现在另一个题中。
输入格式
第一行为正整数 n,m。
接下来 m 行,每行两个正整数 u, v,代表一对「互相排斥」的修改操作。
输出格式
输出一行一个整数,表示毒瘤可以出的可做的不同的数据结构题的个数。这个数可能很大,所以只输出模 998244353 后的值。
样例输入 1
3 2
1 2
2 3
样例输出 1
5
样例输入 2
6 8
1 2
1 3
1 4
2 4
3 5
4 5
4 6
1 6
样例输出 2
16
数据范围与提示
n <= 10^5, m <= n + 10。
@solution@
众所周知图的独立集问题是不可做的,所以我们需要对问题进行合理的暴力搜索。
注意到当 m = n - 1(即一棵树)时用 dp 随便做。
而 m - n 很小,这意味着整张图是一棵树 + 很少的非树边。
算了一下大概非树边最多 11 条,这 11 条边连着最多 22 个特殊点。
于是就有一个大胆的想法:暴力枚举特殊点是否被选中,然后这棵树再 O(n) 做一遍 dp。
暴力枚举的部分看上去是 2^22 种状态,实际上每条边只会对应 3 种状态(不可能一条边连着的两个点同时选),于是只会暴力枚举 3^11 种状态。这个范围小很多。
于是你就可以 O(3^11*n) 写出本题的暴力,约 70 分的好成绩。
要是我每次可以不重新算整棵树的 dp 就好了。
如果特殊点将原树分成了互不相关的若干连通块,且每个连通块只会受 1 或 2 个特殊点影响就好了。
这样我就可以预处理,就不用每次枚举完再重新做一遍 dp。
那我们就通过一些手段将这棵树分成若干连通块:使用虚树。
建出特殊点之间的虚树,虚树上的点将原图分成若干连通块。这样的话,要么是虚树上一条边对应一个连通块,要么一个连通块属于虚树上的某个点管辖。
这样只需要再在虚树上做一遍树形 dp,将预处理出来的连通块信息当作边权/点权即可。
虚树上只有最多 22*2 个点,所以可以轻松过。
@accepted code@
#include<map>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define rep(G, x) for(Graph::edge *p=G.adj[x];p;p=p->nxt)
const int MAXN = 100000;
const int MOD = 998244353;
inline int add(int x, int y) {return (x + y)%MOD;}
inline int mul(int x, int y) {return 1LL*x*y%MOD;}
struct Graph{
struct edge{
int to, f[2][2];
edge *nxt;
}edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt;
Graph() {ecnt = &edges[0];}
void addedge(int u, int v) {
edge *p = (++ecnt);
p->to = v, p->nxt = adj[u], adj[u] = p;
p = (++ecnt);
p->to = u, p->nxt = adj[v], adj[v] = p;
// printf("! %d %d\n", u, v);
}
}G1, G2;
int fa[20][MAXN + 5], dep[MAXN + 5], tid[MAXN + 5], dcnt = 0;
void dfs(int x, int f) {
fa[0][x] = f, tid[x] = (++dcnt);
for(int i=1;i<20;i++)
fa[i][x] = fa[i-1][fa[i-1][x]];
dep[x] = dep[f] + 1;
rep(G1, x) {
if( p->to == f ) continue;
dfs(p->to, x);
}
}
int lca(int u, int v) {
if( dep[u] < dep[v] ) swap(u, v);
for(int i=19;i>=0;i--)
if( dep[fa[i][u]] >= dep[v] )
u = fa[i][u];
if( u == v ) return u;
for(int i=19;i>=0;i--)
if( fa[i][u] != fa[i][v] )
u = fa[i][u], v = fa[i][v];
return fa[0][u];
}
int sfa[MAXN + 5];
int find(int x) {
return sfa[x] = (sfa[x] == x ? x : find(sfa[x]));
}
bool unite(int x, int y) {
int fx = find(x), fy = find(y);
if( fx == fy ) return false;
else {
sfa[fx] = fy;
return true;
}
}
bool tag[MAXN + 5];
bool cmp(int x, int y) {return tid[x] < tid[y];}
vector<int>arr;
int stk[MAXN + 5], tp;
void insert(int x) {
if( tp ) {
int z = lca(stk[tp], x);
while( tp ) {
int y = stk[tp--]; tag[y] = true;
if( !tp || dep[stk[tp]] < dep[z] ) {
if( y != z ) G2.addedge(z, y);
break;
}
else G2.addedge(stk[tp], y);
}
stk[++tp] = z;
}
stk[++tp] = x;
}
int build_vtree() {
sort(arr.begin(), arr.end(), cmp);
for(int i=0;i<arr.size();i++)
insert(arr[i]);
int ret;
while( tp ) {
ret = stk[tp--], tag[ret] = true;
if( tp ) G2.addedge(stk[tp], ret);
}
return ret;
}
int dp[2][MAXN + 5];
void dfs2(int x) {
tag[x] = true, dp[0][x] = dp[1][x] = 1;
rep(G1, x) {
if( !tag[p->to] ) {
dfs2(p->to);
dp[0][x] = mul(dp[0][x], add(dp[0][p->to], dp[1][p->to]));
dp[1][x] = mul(dp[1][x], dp[0][p->to]);
}
}
}
void dfs3(int x, int f) {
dp[0][x] = dp[1][x] = 1;
rep(G1, x) {
if( p->to != f ) {
if( !tag[p->to] ) dfs3(p->to, x);
dp[0][x] = mul(dp[0][x], add(dp[0][p->to], dp[1][p->to]));
dp[1][x] = mul(dp[1][x], dp[0][p->to]);
}
}
}
void func1(int x, int y, int f[][2]) {
int p = fa[0][y];
if( p == x ) {
f[0][0] = f[0][1] = f[1][0] = 1;
return ;
}
dp[0][x] = 1, dp[1][x] = 0;
dfs3(p, y), f[0][0] = add(dp[0][p], dp[1][p]), f[0][1] = dp[0][p];
dp[0][x] = 0, dp[1][x] = 1;
dfs3(p, y), f[1][0] = add(dp[0][p], dp[1][p]), f[1][1] = dp[0][p];
dfs2(p);
}
int g[2][MAXN + 5];
void get_value(int x, int f) {
rep(G2, x) {
if( p->to == f ) continue;
func1(x, p->to, p->f), get_value(p->to, x);
}
g[0][x] = g[1][x] = 1;
rep(G1, x) {
if( !tag[p->to] ) {
dfs2(p->to);
g[0][x] = mul(g[0][x], add(dp[0][p->to], dp[1][p->to]));
g[1][x] = mul(g[1][x], dp[0][p->to]);
}
}
}
int clr[MAXN + 5], c[MAXN + 5], root, ans;
vector<int>vec[MAXN + 5];
void check(int x, int fa) {
rep(G2, x) {
if( p->to != fa )
check(p->to, x);
}
if( clr[x] != -1 )
dp[clr[x]][x] = g[clr[x]][x], dp[!clr[x]][x] = 0;
else dp[0][x] = g[0][x], dp[1][x] = g[1][x];
rep(G2, x) {
if( p->to != fa ) {
dp[0][x] = mul(dp[0][x], add(mul(p->f[0][0], dp[0][p->to]), mul(p->f[0][1], dp[1][p->to])));
dp[1][x] = mul(dp[1][x], add(mul(p->f[1][0], dp[0][p->to]), mul(p->f[1][1], dp[1][p->to])));
}
}
}
void search(int d) {
if( d == arr.size() ) {
check(root, 0);
ans = add(ans, add(dp[0][root], dp[1][root]));
return ;
}
clr[arr[d]] = 0, search(d + 1);
if( !c[arr[d]] ) {
for(int i=0;i<vec[arr[d]].size();i++)
c[vec[arr[d]][i]]++;
clr[arr[d]] = 1, search(d + 1);
for(int i=0;i<vec[arr[d]].size();i++)
c[vec[arr[d]][i]]--;
}
}
map<int, int>mp;
int index(int x) {
if( mp.count(x) ) return mp[x];
else {
arr.push_back(x);
return mp[x] = arr.size() - 1;
}
}
int main() {
int n, m; scanf("%d%d", &n, &m);
for(int i=1;i<=n;i++) sfa[i] = i;
for(int i=1;i<=m;i++) {
int u, v; scanf("%d%d", &u, &v);
if( unite(u, v) ) G1.addedge(u, v);
else {
index(u), index(v);
vec[u].push_back(v);
vec[v].push_back(u);
}
}
if( m == n - 1 ) {
dfs2(1), printf("%d\n", add(dp[0][1], dp[1][1]));
return 0;
}
dfs(1, 0), root = build_vtree(), get_value(root, 0);
for(int i=1;i<=n;i++) c[i] = 0, clr[i] = -1;
search(0);
printf("%d\n", ans);
}
@details@
虽然说着挺简单,但还是写了 190+ 行的代码。
所以写暴力大概是最划算的选择。
所以注意区分 连通块属于虚树上的一条边 与 连通块属于虚树上一个点。