首先發出題目鏈接:
鏈接:https://ac.nowcoder.com/acm/contest/883/A
來源:牛客網
涉及:分塊,離線算法
點擊這裏回到2019牛客暑期多校訓練營解題—目錄貼
題目如下
先說一下題目意思:給你一個無向圖,圖中有個頂點和條邊,每個頂點和每條邊都有一個編號。
表示與點直接相連的所有點的集合。然後有條兩種類型的命令:
1.改變某一個編號區間內的邊的狀態,即對於區間內每一條邊,當這條邊存在的時候就刪除它,刪除後就不存在;當這條邊不存在的時候就加上它,加上它就存在了。
2.判斷與點和點由且只由一條邊相連的所有點是否相等(即判斷與是否相同)。
我們需要定量的來表示。可以給每一個點一個獨有的hash值來表示此點。那麼可以定量爲與點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條邊,我們可以分成塊。除最後一塊以外,其他塊中的邊的數量相同。
由於每一塊中,每條邊所連的兩個點各不相同,可以用一個二維數組來表示每一塊對於塊內所連點的貢獻(表示第塊對點的貢獻),初始化爲0,比如說:
當第塊內有一條邊連接了點和點,
那麼 且
由於一開始所有的邊都是存在的,我們首先對數組進行初始化,同時用一個變量來表示真正一共分了多少塊。
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數組單獨進行更新(假設與相連,如果這條邊被刪除了,那麼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數組,其他的只是對一些塊只打了標記,而沒有處理,所以可以在詢問的時候進行離線處理。
如果訪問某兩個點和的點集是否相同,我們創建兩個兩個臨時變量和,分別賦值爲當前的和。然後遍歷每一個塊,如果這個塊的標記爲1表示這個塊被翻轉過,於是需要異或上這個塊對於點u或者點v的貢獻,異或既可表示刪除貢獻也可表示加上貢獻(block數組)。
即對於一個點點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的貢獻,另外有些貢獻只對塊打了標記。但有了離線處理,就不需要考慮被打標記塊中每一條邊所連接的每一組點,只需考慮當前所給的和點即可。
代碼如下:
#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;
}