2019牛客暑期多校訓練營(第三場)----A-Graph Games

首先發出題目鏈接:
鏈接:https://ac.nowcoder.com/acm/contest/883/A
來源:牛客網
涉及:分塊,離線算法

點擊這裏回到2019牛客暑期多校訓練營解題—目錄貼


題目如下
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
先說一下題目意思:給你一個無向圖,圖中有nn個頂點和mm條邊,每個頂點和每條邊都有一個編號
S(x)S(x)表示與點xx直接相連的所有點的集合。然後有qq條兩種類型的命令:

1.改變某一個編號區間內的邊的狀態,即對於區間內每一條邊,當這條邊存在的時候就刪除它,刪除後就不存在;當這條邊不存在的時候就加上它,加上它就存在了。

2.判斷與點uu和點vv由且只由一條邊相連的所有點是否相等(即判斷S(u)S(u)S(v)S(v)是否相同)。


我們需要定量的來表示S(x)S(x)。可以給每一個點一個獨有的hash值來表示此點。那麼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條邊,我們可以分成(int)sqrt(m)(int)sqrt(m)塊。除最後一塊以外,其他塊中的邊的數量相同。

由於每一塊中,每條邊所連的兩個點各不相同,可以用一個二維數組blockblock來表示每一塊對於塊內所連點的貢獻(block[i][j]block[i][j]表示第ii塊對點jj的貢獻),初始化爲0,比如說:

當第cntcnt塊內有一條邊連接了點uu和點vv
那麼 block[cnt][u]=Hash[v]block[cnt][u] \land =Hash[v]block[cnt][v]=Hash[u]block[cnt][v]\land=Hash[u]

由於一開始所有的邊都是存在的,我們首先對blockblock數組進行初始化,同時用一個變量cntcnt來表示真正一共分了多少塊。

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數組單獨進行更新(假設vvuu相連,如果這條邊被刪除了,那麼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數組,其他的只是對一些塊只打了標記,而沒有處理,所以可以在詢問的時候進行離線處理

如果訪問某兩個點uuvv的點集是否相同,我們創建兩個兩個臨時變量hash1hash1hash2hash2,分別賦值爲當前的sign[u]sign[u]sign[v]sign[v]。然後遍歷每一個塊,如果這個塊的標記爲1表示這個塊被翻轉過,於是需要異或上這個塊對於點u或者點v的貢獻,異或既可表示刪除貢獻也可表示加上貢獻(block數組)。

即對於一個點點v,關於這個點真正的點集S(v)S(v)爲:
S(v)=i=1cntflag[i]=1block[i][v]sign[v]S(v)=\underset{flag[i]=1}{\oplus_{i=1}^{cnt}} block[i][v] \oplus 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的貢獻,另外有些貢獻只對塊打了標記。但有了離線處理,就不需要考慮被打標記塊中每一條邊所連接的每一組點,只需考慮當前所給的uuvv點即可。


代碼如下:

#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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章