Problem

Description

从前有一名毒瘤。

毒瘤最近发现了量产毒瘤题的奥秘。考虑如下类型的数据结构题:给出一个数组,要求支持若干种奇奇怪怪的修改操作(例如给一个区间内的数同时加上 \(c\),或者将一个区间内的数同时开平方根),并且支持询问区间的和。毒瘤考虑了 \(n\) 个这样的修改操作,并将它们编号为 \(1 \ldots n\)。当毒瘤要出数据结构题的时候,他就将这些修改操作中选若干个出来,然后出成一道题。

当然了,这样出的题有可能不可做。通过精妙的数学推理,毒瘤揭露了这些修改操作之间的关系:有 \(m\) 对「互相排斥」的修改操作,第 \(i\) 对是第 \(u_i\) 个操作和第 \(v_i\) 个操作。当一道题中同时含有 \(u_i\) 和 \(v_i\) 这两个操作时,这道题就会变得不可做。另一方面,当一道题中不包含任何「互相排斥」的操作时,这个题就是可做的。此外,毒瘤还发现了一个规律:\(m − n\) 是一个很小的数字(参见「数据范围」中的说明),且任意两个修改操作都是连通的。两个修改操作 \(a, b\) 是连通的,当且仅当存在若干操作 \(t_0, t_1, ... , t_l\),使得 \(t_0 = a,t_l = b\),且对任意 \(1 \le i \le l\),\(t_{i−1}\) 和 \(t_i\) 都是「互相排斥」的修改操作。

一对「互相排斥」的修改操作称为互斥对。现在毒瘤想知道,给定值 \(n\) 和 \(m\) 个互斥对,他一共能出出多少道可做的不同的数据结构题。两个数据结构题是不同的,当且仅当其中某个操作出现在了其中一个题中,但是没有出现在另一个题中。

Input Format

第一行为正整数 \(n, m\)。

接下来 \(m\) 行,每行两个正整数 \(u, v\),代表一对「互相排斥」的修改操作。

Output Format

输出一行一个整数,表示毒瘤可以出的可做的不同的数据结构题的个数。这个数可能很大,所以只输出模 \(998244353\) 后的值。

Sample

Input 1

3 2
1 2
2 3

Output 1

5

Input 2

6 8
1 2
1 3
1 4
2 4
3 5
4 5
4 6
1 6

Output 2

16

Input 3

12 18
12 6
3 11
8 6
2 9
10 4
1 8
6 2
11 5
10 6
12 2
9 3
7 6
2 7
3 2
7 3
5 6
2 11
12 1

Output 3

248

Range

测试点 # 1~4 5~6 7~8 9 10~11 12~14 15~16 17~20
\(n \le\) \(20\) \(10^5\) \(10^5\) \(3000\) \(10^5\) \(3000\) \(10^5\) \(10^5\)
\(m \le\) \(n + 10\) \(n - 1\) \(n\) \(n + 1\) \(n + 1\) \(n + 10\) \(n + 7\) \(n + 10\)

Algorithm

\(DP\),虚树

Mentality

这题真的是,题如其名,我 \(tm\) 码了 \(3.4k......\) 。

我们先来考虑暴力 \(80pts\) (实际上有 \(85pts\) 呢) 。

\(DP\) 式很显然:

\[ f[i][0]=\prod (f[son][0]+f[son][1])\\ f[i][1]=\prod f[son][0] \]

当然,\(Ans=f[1][0]+f[1][1]\) 。

不过我们还多出来一些非树边,怎么办?其实很简单,由于非树边两端点会互相影响,那我们只需要枚举每个与非树边相连的点是选还是不选,然后将 \(DP\) 数组的相关值改为 \(0\) ,再做一遍 \(DP\) 即可。

由于每个点的情况与非树边相关,我们只需要枚举每条非树边的左端点 \(u\) (输入中先输入的那个端点) 是选还是不选,如果选,那么将 \(f[u][0]\) 赋值为 \(0\) ,因为我们已经钦定此点会被选择;同理 \(f[v][1]\) 也要赋值为 \(0\) 。而如果 \(u\) 不选,那就不需要再管 \(v\) 了,因为 \(v\) 不受影响。

枚举部分代码如下:

for(int S=0;S<(1<<top);S++)//top 是非树边个数
{for(int i=1;i<=n;i++)f[i][0]=f[i][1]=1;for(int i=1;i<=top;i++)//相关值赋为 0if(S&(1<<(i-1)))f[U[i]][0]=0,f[V[i]][1]=0;elsef[U[i]][1]=0;DP();//DPans=(ans+(f[1][0]+f[1][1])%mod)%mod;//加入答案
}

那对于 \(100\) 分的部分分怎么做呢?

其实做过暴力的话,也差不多能想到该优化哪个方面了:每次枚举之后的 \(DP\) 。

因为每次只改变了至多 \(22\) 个点的状态,所以我们应该想办法避免重复计算那些无关的点的 \(DP\) 值。

那显然是 动态dp 建虚树啊 。

那么如何优化点与点的 \(DP\) 计算呢?我们可以发现一件事情:由于 \(DP\) 过程中,我们的运算都是乘法运算,所以在虚树上若有边 \(u->v\) ,则我们必定可以得到

\[ f[u][0]=a×f[v][0]+b×f[v][1]\\ f[u][1]=c×f[v][0]+d×f[v][1] \]

其中 \(a,b,c,d\) 均为可以计算的未知数,不妨将其称之为 \(v\) 在虚树上转移的系数。

我们分别设为 \(k0[v][0],k0[v][1]\) 代表 \(f[v][0]\) 分别为 \(f[u][0],f[u][1]\) 有多少系数的贡献; \(k1[v][0],k1[v][1]\) 同理。

这部分的式子及代码如下:

for(int i=x;fa[i][0]!=y;i=fa[i][0])
{int Fa=fa[i][0];work(Fa,i);//计算每层节点不含虚树点的子树的 dp 值int t0=k0[x][0],t1=k1[x][0];k0[x][0]=1ll*f[Fa][0]*(t0+k0[x][1])%mod;k1[x][0]=1ll*f[Fa][0]*(t1+k1[x][1])%mod;k0[x][1]=1ll*f[Fa][1]*t0%mod;k1[x][1]=1ll*f[Fa][1]*t1%mod;
}

那么思路就很简单了,求出虚树上每个点到父结点的实际子节点的转移系数,然后 \(DP\) 的时候利用转移系数 \(DP\) 就好。由于虚树的性质,每个点的一棵虚树子树内只会有一个直接相连的点,否则子树内的两个点的 \(lca\) 也会是关键点 \(......\) 所以不用担心转移问题。

求系数详见代码。

虽然题解超级不详细 (没办法题目毒得我不知何去何从) 。

Code

#include <algorithm>
#include <cstdio>
#include <iostream>
using namespace std;
const int mod = 998244353;
int n, m, ans, fa[100001][18], head[100001], nx[200001], to[200001];
int now, top, cnt, sum, sumk, num, key[23], U[23], V[23], tree[100001],stack[100001], dfn[100001], deep[100001];
int hd2[100001], nx2[100001], to2[100001], g[100001][2], f[100001][2],k0[100001][2], k1[100001][2];
bool vis[100001], book[100001];
int find(int x) { return fa[x][0] == x ? x : fa[x][0] = find(fa[x][0]); }
bool cmp(int a, int b) { return dfn[a] < dfn[b]; }
void addroad(int u, int v, int d) {to[d] = v, nx[d] = head[u];head[u] = d;
}
void build(int x, int pa) {deep[x] = deep[pa] + 1, dfn[x] = ++cnt, fa[x][0] = pa;for (int i = 1; i <= 17; i++) fa[x][i] = fa[fa[x][i - 1]][i - 1];for (int i = head[x]; i; i = nx[i])if (to[i] != pa) build(to[i], x);
}
int getlca(int a, int b) {if (deep[a] < deep[b]) swap(a, b);for (int i = 17; i >= 0; i--)if (deep[fa[a][i]] >= deep[b]) a = fa[a][i];for (int i = 17; i >= 0; i--)if (fa[a][i] != fa[b][i]) a = fa[a][i], b = fa[b][i];return a == b ? a : fa[a][0];
}
void link(int a, int b) {if (!book[a]) tree[++num] = a;if (!book[b]) tree[++num] = b;book[a] = book[b] = true, cnt++;to2[cnt] = b, nx2[cnt] = hd2[a];hd2[a] = cnt;
}
void Insert(int x) {int lca = getlca(stack[top], x);while (top > 1 && dfn[stack[top - 1]] >= dfn[lca])link(stack[--top], stack[top]);if (lca != stack[top]) link(lca, stack[top]), stack[top] = lca;stack[++top] = x;
}
void work(int x,int y)  //正常计算 dp 值,但是不会计算标记点,标记点一般为含虚树点的子树
{f[x][0] = f[x][1] = vis[x] = 1;for (int i = head[x]; i; i = nx[i])if (to[i] != fa[x][0] && to[i] != y && !vis[to[i]]) {work(to[i], y);f[x][0] = 1ll * f[x][0] * (f[to[i]][0] + f[to[i]][1]) % mod;f[x][1] = 1ll * f[x][1] * f[to[i]][0] % mod;}
}
void getk(int x, int y) {vis[x] = k0[x][0] = k1[x][1] = 1;for (int i = x; fa[i][0] != y; i = fa[i][0]) {int Fa = fa[i][0];work(Fa,i);  //层层计算每层的答案,每个节点不含虚树点的子树的 dp 值也会产生贡献int t0 = k0[x][0], t1 = k1[x][0];k0[x][0] = 1ll * f[Fa][0] * (t0 + k0[x][1]) % mod;k1[x][0] = 1ll * f[Fa][0] * (t1 + k1[x][1]) % mod;k0[x][1] = 1ll * f[Fa][1] * t0 % mod;k1[x][1] = 1ll * f[Fa][1] * t1 % mod;}
}
void Count(int x) {for (int i = hd2[x]; i; i = nx2[i])Count(to2[i]), getk(to2[i], x);  //计算系数f[x][0] = f[x][1] = 1;for (int i = head[x]; i; i = nx[i])if (!vis[to[i]] && to[i] != fa[x][0]) {work(to[i], 0);f[x][0] = 1ll * f[x][0] * (f[to[i]][0] + f[to[i]][1]) % mod;f[x][1] = 1ll * f[x][1] * f[to[i]][0] % mod;}  //计算非虚树部分的 dp 值
}
void DP(int x) {for (int i = hd2[x]; i; i = nx2[i]) {int p = to2[i];DP(p);int f0 = (1ll * k0[p][0] * g[p][0] + 1ll * k1[p][0] * g[p][1] % mod) % mod;int f1 = (1ll * k0[p][1] * g[p][0] + 1ll * k1[p][1] * g[p][1] % mod) % mod;g[x][0] = 1ll * g[x][0] * (f0 + f1) % mod,g[x][1] = 1ll * g[x][1] * f0 % mod;  //直接乘系数计算就好了}
}
int main() {cin >> n >> m;int u, v;for (int i = 1; i <= n; i++) fa[i][0] = i;vis[1] = true;for (int i = 1; i <= m; i++) {scanf("%d%d", &u, &v);if (find(u) == find(v)) {sum++;U[sum] = u;if (!vis[u]) key[++sumk] = u;V[sum] = v;if (!vis[v]) key[++sumk] = v;vis[u] = vis[v] = true;}  //利用并查集判断那些边是非树边else {addroad(u, v, ++cnt), addroad(v, u, ++cnt);fa[find(v)][0] = fa[u][0];}}build(1, 0);sort(key + 1, key + sumk + 1, cmp);cnt = 0, stack[top = 1] = 1;for (int i = 1; i <= n; i++) vis[i] = 0;for (int i = 1; i <= sumk; i++) Insert(key[i]);  //构建虚树while (top > 0) link(stack[--top], stack[top]);Count(1);  //计算系数,并预处理每个节点不计算含有虚树的子树的 dp 值for (int S = 0; S < (1 << sum); S++) {for (int i = 1; i <= num; i++)g[tree[i]][0] = f[tree[i]][0], g[tree[i]][1] = f[tree[i]][1];  //赋初值for (int i = 1; i <= sum; i++)  //枚举状态的相关赋值if (S & (1 << (i - 1)))g[U[i]][0] = 0, g[V[i]][1] = 0;elseg[U[i]][1] = 0;DP(1);                                          // DPans = (ans + (g[1][0] + g[1][1]) % mod) % mod;  //计算答案}cout << ans;
}

转载于:https://www.cnblogs.com/luoshuitianyi/p/10574909.html

【HNOI 2018】毒瘤相关推荐

  1. 【HNOI 2018】游戏

    [HNOI 2018]游戏 Problem Description 一次小 \(G\) 和小 \(H\) 在玩寻宝游戏,有 \(n\) 个房间排成一列,编号为 \(1,2,-,n\),相邻房间之间都有 ...

  2. HNOI 2018 游记

    前言 为什么我又现在才写一个星期前的游记QAQ,马上都要自己省的省选了还来这里吹水... 还有,这是今年第几次来长沙了?.. 为什么一点紧张感都没有啊,吃枣药丸啊 Day0 上午坐高铁出发,几乎全程补 ...

  3. 其他-私人♂收藏(比赛记录 Mar, 2019)

    OwO 03.03 [USACO19JAN] A. Redistricting 题意:给 \(g\) ,求 \(f(n)\) . \(f(i)=f(j)+[g(i)\ge g(j)],j \in (i ...

  4. python pip升级为什么升级不了_python pip升级失败

    Python微信-- 分享接口(分享到朋友圈.朋友.空间) 生成JS-SDK权限验证的签名 获取signature(签名)首先要获得 1.#获得jsapi_ticket 2.#获取当前页面的url # ...

  5. python标识符最大可能长度_Opencv-Python学习笔记(二)

    2. 使用OpenCV3处理图像 2.1 不同色彩空间的转换 OpenCV中有数百种关于在不同色彩空间之间转换的方法. 三种常用色彩空间:灰度.BGR.HSV(Hue色调,Saturation饱和度, ...

  6. 2018.08.20高二互测

    2018.08.20 NOIp模拟赛 GKK大佬出的毒瘤题,烧脑.全是原题就不要密码保护了. 第一题 T1链接 ​ 一张图,每条边有代价也有限制,遍历过的点可以解锁这些限制,求最短路.这是一道套路题, ...

  7. $NOIP 2018 PJ游记[ZJ]$

    众所周知 NOIP = NOI plus. NOIP ZJ-PJ 2018 以下仅为游记 . 题解预留.- >T3 T4 (T1T2太水了不放了) 定位 杭州学军中学紫荆港校区(浙江赛区) da ...

  8. 洛谷日报索引(2020、2019、2018)

    历年洛谷日报索引 2020 2019 2018 感觉洛谷日报全是干货!!!先记下来再说 2020 年洛谷日报索引 3 月 #260[dove]Church 编码(和 Lambda 演算) https: ...

  9. NOIP 2018 游记

    Preface 今年的NOIP- Emmmm 某乎上各路大牛们已经评价了不少了,本蒟蒻就不作评价了. Day 0 – Nov 9th,2018 20:30才到酒店,窗户外面就是跟房间差不多高的立交?谜 ...

  10. 2018.10.9模拟赛

    2018.10.9模拟赛 T1 trade 正解:贪心 据说lyd讲过但并没有印象QAQ,考场上现推浪费了不少时间 其实就开个小根堆,每次把堆顶取出来看它是不是比当前的 a[i]a[i]a[i] 小, ...

最新文章

  1. ProjectEuler 4
  2. PAT甲级1009 Product of Polynomials:[C++题解]多项式乘法、高精度乘法
  3. Building wheel for wrapt (setup.py) ... error的解决办法(图文)
  4. Linux 进程内 全局看见,Android获得全局进程信息以及进程使用的内存情况
  5. java根据pdf模板生成pdf_Java 复制、压缩PDF文档
  6. 【MyBatis】Mybatis实现分页效果
  7. WindowsAPI-Findwindow函数和FindWindowEx用法
  8. 前端学习(2319):angular2概述
  9. Docker学习总结(22)——Docke run命令详解
  10. 对应sql建表_图解SQL面试题:如何查找工资前三高的员工
  11. oracle 替换全部空格
  12. 爬虫套路知多少?反爬策略是关键
  13. 野村证券分析师称微软不会收购雅虎和RIM
  14. 现代软件工程 第一周博客作业
  15. 文件搜索工具Listary中文版快速上手使用教程
  16. matlab多排图例,在Matlab中绘制多行的图例
  17. TortoiseGit安装和使用的图文教程
  18. PowerBuilder方式下图像存储功能的研究
  19. Ubuntu桌面卡死解决办法
  20. Python和JavaScript间代码转换4个工具

热门文章

  1. 百度地图API V2.0 离线版本
  2. 揭开均线系统的神秘面纱_揭开动态规划的神秘面纱
  3. Linux性能优化大杀器—平均负载率详解(鼓励收藏)
  4. 球相交的表面积并/体积并
  5. 世界上有10种人,一种是懂二进制的人,一种是不懂二进制的人。
  6. 大数据时代的小数字感:数据再有价值,别人听不懂照样没用
  7. 网贷黑名单查询,通过身份证号,或者手机号查询自己的网贷情况。
  8. P2P网贷行业的新征程
  9. C语言程序写99乘法表,如何用C语言编程序:九九乘法表
  10. C#对.CSV格式的文件--逗号分隔值文件 的读写操作及上传ftp服务器操作方法总结