[Codeforces 639F] Bear and Chemistry(Tarjan+虚树)

题面

给出一个n个点,m条边的无向图(不保证连通,可能有自环和重边),有q次询问,每次询问给出p个点和q条边,判断加上q条边后,这p个点中的任意一个点对(x,y)是否都满足:能从x走到y,再从y走到x,不经过重复的边。询问强制在线。

\((1 ≤ n, q≤ 300 000, 0 ≤ m ≤ 300 000,\sum p \leq 300\ 000,\sum q \leq 300\ 000)\)

分析

我们发现,判断条件等价于:加上q条边后,这p个点在同一个边-双联通分量(e-dcc)中。那么问题就变为如何求出新图的边-双连通分量。直接tarjan时间复杂度为\(O(nq)\)显然是不行的。

首先,原图中处在同一个e-dcc中的点,加完边后肯定也在同一个e-dcc中。因此我们将原图中的e-dcc缩成一个点,那么原图就变成了一个树(或森林)。然后考虑询问,显然只有跟询问有关的点才有意义,因此我们求出p个询问点,还有q条边的端点的e-dcc编号,建立虚树或虚森林( 好像没有这个名词)。最后把q条边加进森林,对这个新图求e-dcc。然后就可以直接判断这p个点是否在新图的同一个e-dcc中了。

思路很简单,但代码实现比较复杂,这里讲几个易错的细节:

  1. 强制在线的时候是把u变成(u+rcnt)%n,其中rcnt是之前询问中答案为YES的询问编号之和(不是YES个数!),并且rcnt可能会爆int,注意对rcnt先取模

  2. 建立虚树的时候。输入的节点可能不在同一棵树上。我们按dfs序排序后,其实每棵树的节点都在一个连续的区间内,只要用尺取法找出连续的区间,然后对该区间内的点建立虚树就可以了。

  3. 遍历图的每个节点的时候,不要从1~n枚举,这样会被卡成\(O(nq)\)。对于每个图维护一个数组记录节点序列,只要遍历这个数组就可以了

  4. 多次调用tarjan和建虚树的函数,一定要注意初始化

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<stack>
#include<cmath>
#include<algorithm>
#define maxn 500000
#define maxm 500000
#define maxlogn 25
using namespace std;
inline void qread(int &x) {
x=0;
int sign=1;
char c=getchar();
while(c<'0'||c>'9') {
if(c=='-') sign=-1;
c=getchar();
}
while(c>='0'&&c<='9') {
x=x*10+c-'0';
c=getchar();
}
x=x*sign;
}
inline void qprint(long long x) {
if(x<0) {
putchar('-');
qprint(-x);
} else if(x==0) {
putchar('0');
return;
} else {
if(x/10>0) qprint(x/10);
putchar('0'+x%10);
}
} int n,m,q; struct graph {//封装图
struct edge {
int from;
int to;
int next;
} E[maxm*2+5];
int nodes[maxn+5];
//存储节点编号,这样可以节约遍历图中节点的时间,不用从1~n遍历(会被卡成O(n^2)),只需要遍历nodes中节点
int head[maxn+5];
int esz;
int vsz;
void add_node(int x) {
vsz++;
nodes[vsz]=x;
}
void unique_node() {
sort(nodes+1,nodes+1+vsz);
vsz=unique(nodes+1,nodes+1+vsz)-nodes-1;
}
void add_edge(int u,int v) {
esz++;
E[esz].from=u;
E[esz].to=v;
E[esz].next=head[u];
head[u]=esz;
}
graph() {
esz=1;
}
void clear() { //使用后清空
esz=1;
for(int i=1; i<=vsz; i++) head[nodes[i]]=0;
vsz=0;
}
void debug() {
printf("node=%d edge=%d\n",vsz,esz);
for(int i=2; i<=esz; i+=2) {
printf("debug:%d %d\n",E[i].from,E[i].to);
}
}
} G1,G2,G3; namespace e_dcc {//边-双联通分量相关
int tim,cnt;
int dfn[maxn+5];
int low[maxn+5];
bool bridge[maxm*2+5];
int bel[maxn+5];//存储属于第几个e-dcc
bool vis[maxn+5]; void tarjan(int x,int last,graph& G) {
dfn[x]=++tim;
low[x]=dfn[x];
for(int i=G.head[x]; i; i=G.E[i].next) {
int y=G.E[i].to;
if(!dfn[y]) {
tarjan(y,i,G);
low[x]=min(low[x],low[y]);
if(dfn[x]<low[y]) {
bridge[i]=bridge[i^1]=1;
}
} else if(i!=(last^1)) low[x]=min(low[x],dfn[y]);
}
} void dfs_mark(int x,int col,graph& G) {//不走桥边,就能求出e-dcc
bel[x]=col;
vis[x]=1;
for(int i=G.head[x]; i; i=G.E[i].next) {
int y=G.E[i].to;
if(!vis[y]&&!bridge[i]) {
dfs_mark(y,col,G);
}
}
} int get_bel(graph &G) {//求出v-dcc,不缩点,返回v-dcc数量
for(int i=1; i<=G.vsz; i++) {
int u=G.nodes[i];
low[u]=dfn[u]=bel[u]=vis[u]=0;
}
for(int i=1; i<=G.esz; i++) bridge[i]=0;
tim=0;
for(int i=1; i<=G.vsz; i++) {
if(!dfn[G.nodes[i]]) {
tarjan(G.nodes[i],0,G);
}
} cnt=0;
for(int i=1; i<=G.vsz; i++) {
if(!vis[G.nodes[i]]) {
cnt++;
dfs_mark(G.nodes[i],cnt,G);
}
}
return cnt;
} void graph_to_tree(graph &in,graph &out) {//缩点
get_bel(in);
for(int i=2; i<=in.esz; i++) {
if(bel[in.E[i].from]!=bel[in.E[i].to]) {
out.add_edge(bel[in.E[i].from],bel[in.E[i].to]);
}
}
out.vsz=cnt;
}
} vector<int>nodes;//虚树上的点
namespace virtual_tree {//虚树相关
int log2n;
int tim=0;
int dfn[maxn+5];
int deep[maxn+5];
int rt[maxn+5];
int anc[maxn+5][maxlogn+5];
void dfs(int x,int fa,int root,graph &T1) {
dfn[x]=++tim;
rt[x]=root;
deep[x]=deep[fa]+1;
anc[x][0]=fa;
for(int i=1; i<=log2n; i++)anc[x][i]=anc[anc[x][i-1]][i-1];
for(int i=T1.head[x]; i; i=T1.E[i].next) {
int y=T1.E[i].to;
if(y!=fa) {
dfs(y,x,root,T1);
}
}
}
int lca(int x,int y) {
if(deep[x]<deep[y]) swap(x,y);
for(int i=log2n; i>=0; i--) {
if(deep[anc[x][i]]>=deep[y]) {
x=anc[x][i];
}
}
if(x==y) return x;
for(int i=log2n; i>=0; i--) {
if(anc[x][i]!=anc[y][i]) {
x=anc[x][i];
y=anc[y][i];
}
}
return anc[x][0];
} int top=0;
int s[maxn];
int in[maxn];
int cmp(int x,int y) {
return dfn[x]<dfn[y];
} void insert(int x,graph &T2) {//虚树插入模板
if(top<1) {
s[++top]=x;
return;
}
int lc=lca(x,s[top]);
if(lc==s[top]) {
s[++top]=x;
return;
}
while(top>1&&deep[s[top-1]]>=deep[lc]) {
T2.add_edge(s[top-1],s[top]);
T2.add_edge(s[top],s[top-1]);
T2.add_node(s[top]);
T2.add_node(s[top-1]);
top--;
}
if(s[top]!=lc) {
T2.add_edge(lc,s[top]);
T2.add_edge(s[top],lc);
T2.add_node(lc);
T2.add_node(s[top]);
}
s[top]=lc;
s[++top]=x;
} void ini_tree(graph &in) { //初始化原来的森林
log2n=log2(in.vsz)+1;
for(int i=1; i<=in.vsz; i++) {
if(!deep[i]) dfs(i,0,i,in);
}
} void get_vir_tree(int *in,int k,graph &out) { //对于每个询问,建出虚树
sort(in+1,in+1+k,cmp);
int r;
for(int l=1; l<=k; l=r+1) {
//注意不一定只有一棵虚树,按dfn排序后
//rt[i]为点i在虚树里的编号
//[l,r]对应虚树中属于同一棵树的区间
r=l;
while(r<k&&rt[in[r]]==rt[in[r+1]]) r++;//找出在一棵树里的点
top=0;
for(int i=l; i<=r; i++) {
insert(in[i],out);
}
while(top>1) {
out.add_edge(s[top-1],s[top]);
out.add_edge(s[top],s[top-1]);
out.add_node(s[top-1]);
out.add_node(s[top]);
top--;
}
if(l==r) out.add_node(in[l]); //只有一个点的特判
} }
} int org_bel[maxn+5];//点x在原图G1中的e-dcc编号,由于namespace里的不断在变,要保存一下 int org_point[maxn+5]; //询问的点
int point[maxn+5];//用来建虚树的点
bool check(int tn){//检查询问的点是否在同一个e-dcc中
for(int i=2;i<=tn;i++){
if(e_dcc::bel[org_point[i]]!=e_dcc::bel[org_point[1]]) return 0;
}
return 1;
}
int main() {
int u,v;
qread(n);
qread(m);
qread(q);
for(int i=1; i<=n; i++) G1.add_node(i);
for(int i=1; i<=m; i++) {
qread(u);
qread(v);
G1.add_edge(u,v);
G1.add_edge(v,u);
} e_dcc::graph_to_tree(G1,G2);
for(int i=1; i<=n; i++) {
org_bel[i]=e_dcc::bel[i]; }
virtual_tree::ini_tree(G2); int rcnt=0;
int tn,tm,tsz;
for(int i=1; i<=q; i++) {
qread(tn);
qread(tm);
tsz=0;
for(int j=1; j<=tn; j++) {
qread(org_point[j]);
org_point[j]=(org_point[j]+rcnt)%n;//强制在线
if(org_point[j]==0) org_point[j]=n;
org_point[j]=org_bel[org_point[j]];
point[++tsz]=org_point[j];
} for(int j=1; j<=tm; j++) {
qread(u);
qread(v);
u=(u+rcnt)%n;
if(u==0) u=n;
v=(v+rcnt)%n;
if(v==0) v=n;
if(org_bel[v]!=org_bel[u]) {
G3.add_edge(org_bel[u],org_bel[v]);
G3.add_edge(org_bel[v],org_bel[u]);
G3.add_node(org_bel[u]);
G3.add_node(org_bel[v]);
}
point[++tsz]=org_bel[u];
point[++tsz]=org_bel[v];//边上的点也要加进去建虚树
} sort(point+1,point+1+tsz);
tsz=unique(point+1,point+1+tsz)-point-1;
virtual_tree::get_vir_tree(point,tsz,G3);
G3.unique_node();
#ifdef DEBUG
G3.debug();
#endif
e_dcc::get_bel(G3);
if(tn==1||check(tn)) {//不一定只有1个e-dcc,只要询问的tn个点在一个e-dcc中
printf("YES\n");
rcnt+=i;//是加上答案编号,不是+1
rcnt%=n; //防止爆int
} else {
printf("NO\n");
} G3.clear();//清空虚树
}
}

[Codeforces 639F] Bear and Chemistry (Tarjan+虚树)(有详细注释)的更多相关文章

  1. CodeCraft-19 and Codeforces Round #537 (Div. 2) E 虚树 + 树形dp(新坑)

    https://codeforces.com/contest/1111/problem/E 题意 一颗有n个点的树,有q个询问,每次从树挑出k个点,问将这k个点分成m组,需要保证在同一组中不存在一个点 ...

  2. P7737-[NOI2021]庆典【tarjan,虚树】

    正题 题目链接:https://www.luogu.com.cn/problem/P7737 题目大意 给出一张无向图满足若\(x\Rightarrow z,y\Rightarrow z\)那么有\( ...

  3. Codeforces.809E.Surprise me!(莫比乌斯反演 虚树)

    题目链接 \(Description\) 给定一棵树,求\[\frac{1}{n(n-1)/2}\times\sum_{i\in[1,n],j\in[1,n],i\neq j}\varphi(a_i\ ...

  4. 算法笔记--树的直径 && 树形dp && 虚树 && 树分治 && 树上差分 && 树链剖分

    树的直径: 利用了树的直径的一个性质:距某个点最远的叶子节点一定是树的某一条直径的端点. 先从任意一顶点a出发,bfs找到离它最远的一个叶子顶点b,然后再从b出发bfs找到离b最远的顶点c,那么b和c ...

  5. BZOJ.2286.[SDOI2011]消耗战(虚树 树形DP)

    题目链接 BZOJ 洛谷P2495 树形DP,对于每棵子树要么逐个删除其中要删除的边,要么直接断连向父节点的边. 如果当前点需要删除,那么直接断不需要再管子树. 复杂度O(m*n). 对于两个要删除的 ...

  6. Codeforces Round #328 (Div. 2) D. Super M 虚树直径

    D. Super M Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/592/problem/D ...

  7. Codeforces Round #613 Div.1 D.Kingdom and its Cities 贪心+虚树

    题目链接:http://codeforces.com/contest/613/problem/D 题意概述: 给出一棵树,每次询问一些点,计算最少删除几个点可以让询问的点两两不连通,无解输出-1.保证 ...

  8. CodeForces - 592D: Super M(虚树+树的直径)

    Ari the monster is not an ordinary monster. She is the hidden identity of Super M, the Byteforces’ s ...

  9. 【Codeforces 809E】Surprise me!(莫比乌斯反演 & 虚树)

    Description 给定一颗 \(n\) 个顶点的树,顶点 \(i\) 的权值为 \(a_i\).求: \[\frac{1}{n(n-1)}\sum_{i=1}^n\sum_{j=1}^n\var ...

随机推荐

  1. IO流详解及测试代码

    IO流 (1)IO用于在设备间进行数据传输的操作 (2)分类:    A:流向       输入流 读取数据      输出流 写出数据   B:数据类型     字节流         字节输入流  ...

  2. GB28181 To RTMP/HLS/HTTP-FLV/DASH Gateway

    I. Deployment  / Architecture Block Diagram II. Resources Used 1. freeswitch —— sip server https://f ...

  3. Redis 复制原理及特性

    摘要 早期的RDBMS被设计为运行在单个CPU之上,读写操作都由经单个数据库实例完成,复制技术使得数据库的读写操作可以分散在运行于不同CPU之上的独立服务器上,Redis作为一个开源的.优秀的key- ...

  4. cx_Oracle python模块安装

    1. 需要从oracle网站下载一下两个包 instantclient-basic-linux.x64-11.2.0.4.0.zip instantclient-sdk-linux.x64-11.2. ...

  5. [BZOJ2600] ricehub

    问题描述 乡间有一条笔直而长的路称为"米道".沿着这条米道上 R 块稻田,每块稻田的坐标均为一个 1 到 L 之间(含 1 和 L)的整数.这些稻田按照坐标以不减的顺序给出,即对于 ...

  6. 定时任务crond

    在服务器上使用crond完成定时操作很方便, 下面简单记录一下. 常用的命令主要有以下三个: (1)编辑定时任务 crontab -e (2)   查看定时任务 crontab -l (3)  删除定 ...

  7. python学习笔记(七)模块

    一个python文件就是一个模块 1.标准模块 python自带的,不需要你安装的 2.第三方模块 需要安装,别人提供的,例:pip install radis 如果提示没有pip,把python下s ...

  8. Winner

    Winner 南昌邀请赛 暴力模拟 #include<bits/stdc++.h> using namespace std; struct Nod { int i; int a,b,c; ...

  9. 第九届ECNU Coder F.蚂蚁(栈)

    题目链接:http://acm.ecnu.edu.cn/contest/16/problem/F/ 题目: F. 蚂蚁 Time limit per test: 0.5 seconds Time li ...

  10. HTML5 新属性的讲解

    1.选择器: 标签选择器: class选择器: id选择器: 后代选择器:div li div下所有li 子代选择器:div>li div的所有子一代 li 元素 交集选择器:div.class ...