2019牛客暑期多校训练营(第三场)----A-Graph Games

mac2022-06-30  33

首先发出题目链接: 链接:https://ac.nowcoder.com/acm/contest/883/A 来源:牛客网 涉及:分块,离线算法

点击这里回到2019牛客暑期多校训练营解题—目录贴


题目如下 先说一下题目意思:给你一个无向图,图中有 n n n个顶点和 m m m条边,每个顶点和每条边都有一个编号 S ( x ) S(x) S(x)表示与点 x x x直接相连的所有点的集合。然后有 q q q条两种类型的命令:

1.改变某一个编号区间内的边的状态,即对于区间内每一条边,当这条边存在的时候就删除它,删除后就不存在;当这条边不存在的时候就加上它,加上它就存在了。

2.判断与点 u u u和点 v v v由且只由一条边相连的所有点是否相等(即判断 S ( u ) S(u) S(u) S ( v ) S(v) S(v)是否相同)。


我们需要定量的来表示 S ( x ) S(x) S(x)。可以给每一个点一个独有的hash值来表示此点。那么 S ( x ) S(x) S(x)可以定量为与点x直连的点集内所有点的hash值的异或和。

一共n个点,给每一个点一个hash值,可以用随机值来定义:

void random_point() { for(int i = 1; i <= n; i++){ Hash[i] = rand();//Hash[i]表示第i个点的hash值。 } return; }

创建一个数组sign,sign[i]表示当前部分边翻转对于点i的贡献,为什么是部分边,后面再解释。 开始的时候所有边都是存在的,我们要对sign(x)进行初始化

for(int i = 1; i <= m; i++) { scanf("%d%d",&edge[i].point1,&edge[i].point2);//边结构体 sign[edge[i].point1] ^= Hash[edge[i].point2]; sign[edge[i].point2] ^= Hash[edge[i].point1]; } struct Edge{ int point1; int point2; };

由于每次翻转的都是一个区间内的所有边,为了减少复杂度,可以对所有的边进行分块处理,一共有m条边,我们可以分成 ( i n t ) s q r t ( m ) (int)sqrt(m) (int)sqrt(m)块。除最后一块以外,其他块中的边的数量相同。

由于每一块中,每条边所连的两个点各不相同,可以用一个二维数组 b l o c k block block来表示每一块对于块内所连点的贡献( b l o c k [ i ] [ j ] block[i][j] block[i][j]表示第 i i i块对点 j j j的贡献),初始化为0,比如说:

当第 c n t cnt cnt块内有一条边连接了点 u u u和点 v v v, 那么 b l o c k [ c n t ] [ u ] ∧ = H a s h [ v ] block[cnt][u] \land =Hash[v] block[cnt][u]=Hash[v] b l o c k [ c n t ] [ v ] ∧ = H a s h [ u ] block[cnt][v]\land=Hash[u] block[cnt][v]=Hash[u]

由于一开始所有的边都是存在的,我们首先对 b l o c k block block数组进行初始化,同时用一个变量 c n t cnt cnt来表示真正一共分了多少块。

int blocknum = sqrt(m), cnt = 0;//blocknum表示每一块含有多少边,cnt记录一共分了多少块 for(int i = 1; i <= m; i += blocknum) { cnt++;//增加一个块 flag[cnt] = false; //清空标记 l[cnt] = i;//这一块所含边的序号的左边界 r[cnt] = min(i + blocknum - 1, m);//这一块所含边的序号的右边界,同时要对最后一块进行特殊处理。 for(int j = 1; j <= n; j++) block[cnt][j] = 0;//先把初始化block数组 for(int j = l[cnt]; j <= r[cnt]; j++) {//后面再根据实际的边来初始化block数组 block[cnt][edge[j].point1] ^= Hash[edge[j].point2]; block[cnt][edge[j].point2] ^= Hash[edge[j].point1]; } }

于是每次翻转区间内的边:

1.如果某一个块内的边全部都被翻转,我们就给这个块打标记。如果这个块在后面又被翻转了一次,那就删除标记,可以异或来体现标记的添加与删除。

2.如果某一个块内只有一部分的边被翻转,那么就直接暴力求解:对于这一部分边,根据边连接的两个点,对sign数组单独进行更新(假设 v v v u u u相连,如果这条边被删除了,那么sign数组需要再次异或Hash[v]或者Hash[u]来达到点集内删除点的效果)。

if(opt==1) {//翻转的边的序号为x到y int cnt1 = (x - 1) / blocknum + 1;//确定边x在哪一块内 int cnt2 = (y - 1) / blocknum + 1;//确定边y在哪一块内 if(cnt1 + 1 <= cnt2) {//如果边x与边y不在同一块 for(int i = cnt1 + 1; i < cnt2; i++) {//把全部边都翻转的块加上或者减去标记 //flag刚开始为0表示没有被翻转过,如果第一次被反转则异或1相当于打上标记 //如果后面又被全部翻转了一次,相当于还原,异或1则相当于删除表示。 flag[i] ^= 1;//flag即为标记 } //对于两端只有部分边被翻转的块进行暴力操作 for(int i = x; i <= r[cnt1]; i++) { sign[edge[i].point1] ^= Hash[edge[i].point2];//进行异或表示添加或删除 sign[edge[i].point2] ^= Hash[edge[i].point1]; } for(int i = l[cnt2]; i <= y; i++) { sign[edge[i].point2] ^= Hash[edge[i].point1]; sign[edge[i].point1] ^= Hash[edge[i].point2]; } }

然后是关于询问的处理 由于在翻转边的时候,真正进行更新的是sign数组,其他的只是对一些块只打了标记,而没有处理,所以可以在询问的时候进行离线处理。

如果访问某两个点 u u u v v v的点集是否相同,我们创建两个两个临时变量 h a s h 1 hash1 hash1 h a s h 2 hash2 hash2,分别赋值为当前的 s i g n [ u ] sign[u] sign[u] s i g n [ v ] sign[v] sign[v]。然后遍历每一个块,如果这个块的标记为1表示这个块被翻转过,于是需要异或上这个块对于点u或者点v的贡献,异或既可表示删除贡献也可表示加上贡献(block数组)。

即对于一个点点v,关于这个点真正的点集 S ( v ) S(v) S(v)为: S ( v ) = ⊕ i = 1 c n t f l a g [ i ] = 1 b l o c k [ i ] [ v ] ⊕ s i g n [ v ] S(v)=\underset{flag[i]=1}{\oplus_{i=1}^{cnt}} block[i][v] \oplus sign[v] S(v)=flag[i]=1i=1cntblock[i][v]sign[v]

//下面是离线处理 int hash1 = sign[x], hash2 = sign[y];//用两个临时变量储存sign for(int i = 1; i <= cnt; i++) {//遍历每一个块 if(flag[i]) {//如果这个块被打了标记 hash1 ^= block[i][x];//需要异或上这个块对于点x的贡献 hash2 ^= block[i][y];//需要异或上这个块对于点y的贡献 } } printf("%d", (hash1 == hash2));//判断两个点的点集是否相同

由于sign[i]只储存了部分边翻转后对于点i的贡献,另外有些贡献只对块打了标记。但有了离线处理,就不需要考虑被打标记块中每一条边所连接的每一组点,只需考虑当前所给的 u u u v v v点即可。


代码如下:

#include <iostream> #include <cstdlib> #include <cmath> #include <algorithm> using namespace std; typedef long long ll; typedef unsigned long long ull; const int msm = 460;//表示sqrt(m)的最大值,用来判断分块的数量 const int maxm = 2e5; const int maxn = 1e5+5; int t, n, m, q;//题目所给变量 int opt, x, y;//每一条指令的三个参数 struct Edge{//边结构体 int point1; int point2; }; ull block[msm][maxn]; //block[i][j]表示第i块对点j的贡献 ull sign[maxn]; //sign[i]表示第i的点所连点的部分集合 bool flag[msm]; //flag[i]表示第i块的标记 ull Hash[maxn]; //Hash[i]表示第i个点的hash值 Edge edge[maxm]; //edge[i]表示第i条边 int l[msm], r[msm]; //l[i]与r[i]分别表示第i块的左边界和右边界 void random_point() { for(int i = 1; i <= n; i++){ Hash[i] = rand();//Hash[i]表示第i个点的hash值。 } return; } int main() { scanf("%d", &t); while(t--) { scanf("%d%d", &n, &m); random_point();//给每一个点一个hash值 for(int i = 1; i <= n; i++) sign[i] = 0;//初始化sign数组 for(int i = 1; i <= m; i++) { scanf("%d%d",&edge[i].point1,&edge[i].point2); //一开始此边存在,所以更新sign数组 sign[edge[i].point1] ^= Hash[edge[i].point2]; sign[edge[i].point2] ^= Hash[edge[i].point1]; } int blocknum = sqrt(m), cnt = 0;//blocknum表示每一块所含的边的个数,cnt表示一共分了多少块 for(int i = 1; i <= m; i += blocknum) { cnt++;//块数加一 flag[cnt] = false;//清空标记 l[cnt] = i;//更新当前块的左边界 r[cnt] = min(i + blocknum - 1, m);//更新当前块的右边界,注意考虑最后一块的特殊性 for(int j = 1; j <= n; j++) block[cnt][j] = 0;//先把初始化block数组 for(int j = l[cnt]; j <= r[cnt]; j++) {//后面再根据实际的边来初始化block数组 block[cnt][edge[j].point1] ^= Hash[edge[j].point2]; block[cnt][edge[j].point2] ^= Hash[edge[j].point1]; } } scanf("%d", &q); while(q--) { scanf("%d%d%d", &opt, &x, &y); if(opt==1) {//命令类型为1,翻转的边的序号为x到y int cnt1 = (x - 1) / blocknum + 1;//确定边x在哪一块内 int cnt2 = (y - 1) / blocknum + 1;//确定边y在哪一块内 if(cnt1 + 1 <= cnt2) {//如果边x与边y不在同一块 for(int i = cnt1 + 1; i < cnt2; i++) {//把全部边都翻转的块加上或者减去标记 //flag刚开始为0表示没有被翻转过,如果第一次被反转则异或1相当于打上标记 //如果后面又被全部翻转了一次,相当于还原,异或1则相当于删除表示。 flag[i] ^= 1;//flag即为标记 } for(int i = x; i <= r[cnt1]; i++) { sign[edge[i].point1] ^= Hash[edge[i].point2];//进行异或表示添加或删除 sign[edge[i].point2] ^= Hash[edge[i].point1]; } for(int i = l[cnt2]; i <= y; i++) { sign[edge[i].point2] ^= Hash[edge[i].point1]; sign[edge[i].point1] ^= Hash[edge[i].point2]; } } else{//如果区间包含于某一个块 for(int i = x; i <= y; i++) {//直接对区间内的边进行更新 sign[edge[i].point2] ^= Hash[edge[i].point1]; sign[edge[i].point1] ^= Hash[edge[i].point2]; } } } else {//命令类型为2 int hash1 = sign[x], hash2 = sign[y];//用两个临时变量储存sign for(int i = 1; i <= cnt; i++) {//遍历每一个块 if(flag[i]) {//如果这个块被打了标记 hash1 ^= block[i][x];//需要异或上这个块对于点x的贡献 hash2 ^= block[i][y];//需要异或上这个块对于点y的贡献 } } printf("%d", (hash1 == hash2));//判断两个点的点集是否相同 } } puts("");//最后输出此字符串的'\0' } return 0; }
最新回复(0)