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

首先发出题目链接:
链接: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;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值