【超多注释 | 正确题解】洛谷 P4246 [SHOI2008] 堵塞的交通 [线段树维护区间连通性]

发布于:2025-09-10 ⋅ 阅读:(16) ⋅ 点赞:(0)

打眼一看,毫无思路。

我又没学过 LCT 和可撤销并查集,怎么办?

(洛谷上有一些题解把问题复杂化了,于是有了我写的这篇)


题面指路:P4246 [SHOI2008] 堵塞的交通 - 洛谷

前置知识:线段树

解析:

1.先想想

首先,我们得明确要干什么:

添加相邻点之间的边

删除相邻点之间的边

查询两个节点的连通性

注意到只有两行要操作,不如直接枚举路径

part 1:直接从 a 走到 b

(实际可能没这么多弯弯绕绕,我为了好看这样画而已)

part 2:从 a 绕到左边的列再走回 b

part 3:从 a 走到 b 右边的列再绕回来

我们发现,这样可以把问题转化成区间连通性判断

(比如说如果 a 到 k 联通,而 k 到 b 也联通,那么 a 到 b 联通)

而支持区间修改(添加和删除)的数据结构,自然想到了线段树

2.状态设计

以下代码中出现的行默认是based - 0,即题目定义行 - 1。

以下注释中 l,r 叫端点,线段树里的叫节点。

考虑设计线段树节点结构体

#define lc(p) (p << 1)
#define rc(p) (p << 1 | 1)   //我自己喜欢用的左右子节点的宏定义 

const int N = 1e5 + 10;

//线段树端点 
struct node {
	int l, r;           //左右端点 
	int cnt[3][3];    // connect[0/1][0/1]:左端点的 0/1 行和右端点的 0/1 行联通就为 1,反之为 0 
	int ra[3];        //roundabout[0]:从左端点的 0 行绕到区间的中间,再绕回来左端点的 1 行,两点联通为 1,反之为 0 
					 //roundabout[1]:从右端点的 0 行绕到区间的中间,再绕回来右端点的 1 行,两点联通为 1,反之为 0 
} tr[N << 2];        //线段树空间开 4 倍 

建图的时候就这么建:

void build(int p, int l, int r) {   //建树 
	tr[p].l = l; tr[p].r = r;
	tr[p].cnt[0][1] = tr[p].cnt[1][0] = 0;   //初始化 
	tr[p].ra[0] = tr[p].ra[1] = 0;
	
	if (l == r) {  // 当只有一列的时候 
		tr[p].cnt[0][0] = tr[p].cnt[1][1] = 1;
		// 左端点的 0 行和右端点的 0 行联通,左端点的 1 行和右端点的 1 行联通 
		//反正就一列,一行就一个 
		return;
	}
	
	int mid = (l + r) >> 1;
	build(lc(p), l, mid); 
	build(rc(p), mid + 1, r);
	tr[p] = merge(tr[lc(p)], tr[rc(p)]);    //这个函数下面会讲 
}

再设计 merge 更新 & 合并函数

这个是本题最难的部分(其实也一般),不过我的注释很详细!

(以下用 & 连接起来的判断句,其中有一个为 0 整个都为 0)

int n, valc[N], valr[2][N];
// valc[x]:第 x 列上下联通
// valr[r][x]:第 r 行的 x - 1 列 和 x 列联通 
// 因为给出修改的路径的都是相邻的,所以 valr 就很好用,详见主函数 

// 合并函数 
node merge(node le, node re) {   // 输入左右节点 
	node res;               // 新建合并后节点 
	res.l = le.l; res.r = re.r;    // 新节点左右端点赋值 
	
	res.ra[0] = ( le.ra[0] | (le.cnt[0][0] & valr[0][re.l] & re.ra[0] & valr[1][re.l] & le.cnt[1][1]) );
	// ra[0]:我们想从新节点的左端点的 0 行绕到区间的中间,再绕回来左端点的 1 行,两点联通 
	// 如果左节点 le 可以这么做,那就直接可以 
	// 否则要先从 le 左端点的 0 行到 le 右端点的 0 行,第 0 行再从 le 右端点到 re 左端点
	// 判断右节点 re 的 ra[0],再从第 1 行的 re 左端点到 le 右端点,从 le 右端点的 1 行到 le 左端点的 1 行
	 
	res.ra[1] = ( re.ra[1] | (re.cnt[0][0] & valr[0][re.l] & le.ra[1] & valr[1][re.l] & re.cnt[1][1]) );
	// ra[1]:我们想从新节点的右端点的 0 行绕到区间的中间,再绕回来右端点的 1 行,两点联通 
	// 如果右节点 re 可以这么做,那就直接可以 
	// 否则要先从 re 右端点的 0 行到 re 左端点的 0 行,第 0 行再从 re 左端点到 le 右端点
	// 判断左节点 le 的 ra[1],再从第 1 行的 le 右端点到 re 左端点,从 le 左端点的 1 行到 le 右端点的 1 行
	
	res.cnt[0][0] = ( (le.cnt[0][0] & valr[0][re.l] & re.cnt[0][0]) | (le.cnt[0][1] & valr[1][re.l] & re.cnt[1][0]) );
	// cnt[0][0]:我们想要新节点的左端点的 0 行和右端点的 0 行联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 0 行联通,那么就联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 0 行联通,那么就联通 
	
	res.cnt[0][1] = ( (le.cnt[0][0] & valr[0][re.l] & re.cnt[0][1]) | (le.cnt[0][1] & valr[1][re.l] & re.cnt[1][1]) );
	// cnt[0][1]:我们想要新节点的左端点的 0 行和右端点的 1 行联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 1 行联通,那么就联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 1 行联通,那么就联通 
	
	res.cnt[1][0] = ( (le.cnt[1][0] & valr[0][re.l] & re.cnt[0][0]) | (le.cnt[1][1] & valr[1][re.l] & re.cnt[1][0]) );
	// cnt[1][0]:我们想要新节点的左端点的 1 行和右端点的 0 行联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 0 行联通,那么就联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 0 行联通,那么就联通 
	
	res.cnt[1][1] = ( (le.cnt[1][0] & valr[0][re.l] & re.cnt[0][1]) | (le.cnt[1][1] & valr[1][re.l] & re.cnt[1][1]) );
	// cnt[1][1]:我们想要新节点的左端点的 1 行和右端点的 1 行联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 1 行联通,那么就联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 1 行联通,那么就联通 
	
	return res;    //输出合并后新节点 
}

3.整体代码

剩下的操作函数和主函数都很简单,直接在整体代码里看就行。

带注释版本:

#include<bits/stdc++.h>
using namespace std;

#define lc(p) (p << 1)
#define rc(p) (p << 1 | 1)   //我自己喜欢用的左右子节点的宏定义 

const int N = 1e5 + 10;

//线段树端点 
struct node {
	int l, r;           //左右端点 
	int cnt[3][3];    // connect[0/1][0/1]:左端点的 0/1 行和右端点的 0/1 行联通就为 1,反之为 0 
	int ra[3];        //roundabout[0]:从左端点的 0 行绕到区间的中间,再绕回来左端点的 1 行,两点联通为 1,反之为 0 
					 //roundabout[1]:从右端点的 0 行绕到区间的中间,再绕回来右端点的 1 行,两点联通为 1,反之为 0 
} tr[N << 2];        //线段树空间开 4 倍 

int n, valc[N], valr[2][N];
// valc[x]:第 x 列上下联通
// valr[r][x]:第 r 行的 x - 1 列 和 x 列联通 
// 因为给出修改的路径的都是相邻的,所以 valr 就很好用,详见主函数 

// 合并函数 
node merge(node le, node re) {   // 输入左右节点 
	node res;               // 新建合并后节点 
	res.l = le.l; res.r = re.r;    // 新节点左右端点赋值 
	
	res.ra[0] = ( le.ra[0] | (le.cnt[0][0] & valr[0][re.l] & re.ra[0] & valr[1][re.l] & le.cnt[1][1]) );
	// ra[0]:我们想从新节点的左端点的 0 行绕到区间的中间,再绕回来左端点的 1 行,两点联通 
	// 如果左节点 le 可以这么做,那就直接可以 
	// 否则要先从 le 左端点的 0 行到 le 右端点的 0 行,第 0 行再从 le 右端点到 re 左端点
	// 判断右节点 re 的 ra[0],再从第 1 行的 re 左端点到 le 右端点,从 le 右端点的 1 行到 le 左端点的 1 行
	 
	res.ra[1] = ( re.ra[1] | (re.cnt[0][0] & valr[0][re.l] & le.ra[1] & valr[1][re.l] & re.cnt[1][1]) );
	// ra[1]:我们想从新节点的右端点的 0 行绕到区间的中间,再绕回来右端点的 1 行,两点联通 
	// 如果右节点 re 可以这么做,那就直接可以 
	// 否则要先从 re 右端点的 0 行到 re 左端点的 0 行,第 0 行再从 re 左端点到 le 右端点
	// 判断左节点 le 的 ra[1],再从第 1 行的 le 右端点到 re 左端点,从 le 左端点的 1 行到 le 右端点的 1 行
	
	res.cnt[0][0] = ( (le.cnt[0][0] & valr[0][re.l] & re.cnt[0][0]) | (le.cnt[0][1] & valr[1][re.l] & re.cnt[1][0]) );
	// cnt[0][0]:我们想要新节点的左端点的 0 行和右端点的 0 行联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 0 行联通,那么就联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 0 行联通,那么就联通 
	
	res.cnt[0][1] = ( (le.cnt[0][0] & valr[0][re.l] & re.cnt[0][1]) | (le.cnt[0][1] & valr[1][re.l] & re.cnt[1][1]) );
	// cnt[0][1]:我们想要新节点的左端点的 0 行和右端点的 1 行联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 1 行联通,那么就联通 
	// 如果左节点 le 的左端点的 0 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 1 行联通,那么就联通 
	
	res.cnt[1][0] = ( (le.cnt[1][0] & valr[0][re.l] & re.cnt[0][0]) | (le.cnt[1][1] & valr[1][re.l] & re.cnt[1][0]) );
	// cnt[1][0]:我们想要新节点的左端点的 1 行和右端点的 0 行联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 0 行联通,那么就联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 0 行联通,那么就联通 
	
	res.cnt[1][1] = ( (le.cnt[1][0] & valr[0][re.l] & re.cnt[0][1]) | (le.cnt[1][1] & valr[1][re.l] & re.cnt[1][1]) );
	// cnt[1][1]:我们想要新节点的左端点的 1 行和右端点的 1 行联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 0 行联通,第 0 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 0 行和右端点的 1 行联通,那么就联通 
	// 如果左节点 le 的左端点的 1 行和右端点的 1 行联通,第 1 行再从 le 右端点到 re 左端点
	// 并且右节点 re 的左端点的 1 行和右端点的 1 行联通,那么就联通 
	
	return res;    //输出合并后新节点 
}

void build(int p, int l, int r) {   //建树 
	tr[p].l = l; tr[p].r = r;
	tr[p].cnt[0][1] = tr[p].cnt[1][0] = 0;   //初始化 
	tr[p].ra[0] = tr[p].ra[1] = 0;
	
	if (l == r) {  // 当只有一列的时候 
		tr[p].cnt[0][0] = tr[p].cnt[1][1] = 1;
		// 左端点的 0 行和右端点的 0 行联通,左端点的 1 行和右端点的 1 行联通 
		//反正就一列,一行就一个 
		return;
	}
	
	int mid = (l + r) >> 1;
	build(lc(p), l, mid); 
	build(rc(p), mid + 1, r);
	tr[p] = merge(tr[lc(p)], tr[rc(p)]);    //这个函数下面会讲 
}

//这俩操作函数传进去的 x 都是列 
void update(int x, int p) {  //更新函数 
	if (x < tr[p].l || tr[p].r < x) {  // x 不在当前节点范围内 
		return ; 
	}
	if (tr[p].l == tr[p].r) {   //如果这个节点只有一列 
		tr[p].ra[0] = tr[p].ra[1] = tr[p].cnt[0][1] = tr[p].cnt[1][0] = valc[tr[p].l];
		// ra 和 cnt[1/0][0/1] 都指望着上下连不连通了 
		return;
	}
	
	update(x, lc(p));
	update(x, rc(p));
	tr[p] = merge(tr[lc(p)], tr[rc(p)]);    //改完了别忘了合并 
}

node query(int l, int r, int p) {   //询问函数 
	if (l <= tr[p].l && tr[p].r <= r) {   //节点区间被包含在询问范围内 
		return tr[p];
	}
	int mid = (tr[p].l + tr[p].r) >> 1;
	if (r <= mid) {
		return query(l, r, lc(p));
	}
	if (l > mid) {
		return query(l, r, rc(p));
	}
	
	return merge( query(l, r, lc(p)), query(l, r, rc(p)) );   //合并 
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> n;
	
	build(1,1,n);
	
	char s[10];    //开大点,防止奇怪空格 
	while (cin >> s) {
		if (s[0] == 'E') {
			break;
		}
		
		
		int ra, ca, rb, cb;
		cin >> ra >> ca >> rb >> cb;
		-- ra; -- rb;
		
		if (ca > cb) {    //保证列从小到大 
			swap(ra, rb);
			swap(ca, cb);
		}
		
		if(s[0] == 'O'){         //疏通 
			if (ca == cb) {
				valc[ca] = 1;   //单列连通性 
			}
			else {  //这里的 ra 和 rb 是一样的 
				valr[rb][cb] = 1;   //相邻列连通性 
			}
			update(cb, 1);
		}
		
		else if(s[0] == 'C') {   //堵塞 
			if (ca == cb) {
				valc[ca] = 0;
			}
			else {
				valr[rb][cb] = 0;
			}
			update(cb, 1);
		}
		
		else {                   //询问 
			node tmpl = query(1, ca, 1);
			node tmp = query(ca, cb, 1);
			node tmpr = query(cb, n, 1);
			
			if (tmp.cnt[ra][rb]) {  											 //直接连通就再好不过了 
				cout << "Y" << "\n";
			}
			else if (tmp.cnt[ra][rb ^ 1] && tmpr.ra[0]) {  //先连到右端点不同行,再走到询问区间右边改变行,最后回来 
				cout << "Y" << "\n";
			}
			else if (tmp.cnt[ra ^ 1][rb] && tmpl.ra[1]) {  //先连到左端点不同行,再走到询问区间左边改变行,最后回来 
				cout << "Y" << "\n";
			}
			else if (tmp.cnt[ra ^ 1][rb ^ 1] && tmpl.ra[1] && tmpr.ra[0]) {  	//两边都连不同行,两边都改完行再回来 
				cout << "Y" << "\n";
			}
			else {																//实在没招了 
				cout << "N" << "\n";
			}
		}
	}
	
	return 0;
}

不带注释方便调试的版本:

#include<bits/stdc++.h>
using namespace std;

#define lc(p) p << 1
#define rc(p) p << 1 | 1

const int N = 1e5 + 10;

struct node {
	int l, r;
	int cnt[3][3];
	int ra[3];
} tr[N << 2];

int n, valc[N], valr[3][N];

node merge(node le, node re) {
	node res;
	res.l = le.l; res.r = re.r;
	
	res.ra[0] = ( le.ra[0] | (le.cnt[0][0] & valr[0][re.l] & re.ra[0] & valr[1][re.l] & le.cnt[1][1]) );
	res.ra[1] = ( re.ra[1] | (re.cnt[0][0] & valr[0][re.l] & le.ra[1] & valr[1][re.l] & re.cnt[1][1]) );
	
	res.cnt[0][0] = ( (le.cnt[0][0] & valr[0][re.l] & re.cnt[0][0]) | (le.cnt[0][1] & valr[1][re.l] & re.cnt[1][0]) );
	res.cnt[0][1] = ( (le.cnt[0][0] & valr[0][re.l] & re.cnt[0][1]) | (le.cnt[0][1] & valr[1][re.l] & re.cnt[1][1]) );
	res.cnt[1][0] = ( (le.cnt[1][0] & valr[0][re.l] & re.cnt[0][0]) | (le.cnt[1][1] & valr[1][re.l] & re.cnt[1][0]) );
	res.cnt[1][1] = ( (le.cnt[1][0] & valr[0][re.l] & re.cnt[0][1]) | (le.cnt[1][1] & valr[1][re.l] & re.cnt[1][1]) );
	
	return res;
}

void build(int p, int l, int r) {
	tr[p].l = l; tr[p].r = r;
	tr[p].ra[0] = tr[p].ra[1] = 0;
	tr[p].cnt[0][1] = tr[p].cnt[1][0] = 0;
	
	if (l == r) {
		tr[p].cnt[0][0] = tr[p].cnt[1][1] = 1;
		return ;
	}
	int mid = (l + r) >> 1;
	build(lc(p), l, mid);
	build(rc(p), mid + 1, r);
	tr[p] = merge(tr[lc(p)], tr[rc(p)]);
}

void update(int x, int p) {
	if (x < tr[p].l || x > tr[p].r) {
		return ;
	}
	if (tr[p].l == tr[p].r) {
		tr[p].ra[0] = tr[p].ra[1] = tr[p].cnt[0][1] = tr[p].cnt[1][0] = valc[tr[p].l];
		return ;
	}
	update(x, lc(p));
	update(x, rc(p));
	tr[p] = merge(tr[lc(p)], tr[rc(p)]);
}

node query(int l, int r, int p) {
	if (l <= tr[p].l && tr[p].r <= r) {
		return tr[p];
	}
	int mid = (tr[p].l + tr[p].r) >> 1;
	if (r <= mid) {
		return query(l, r, lc(p));
	}
	if(l > mid) {
		return query(l, r, rc(p));
	}
	return merge(query(l, r, lc(p)), query(l, r, rc(p)));
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin >> n;
	
	build(1, 1, n);
	
	char s[10];
	while (cin >> s) {
		if (s[0] == 'E') {
			break;
		}
		
		int ra, ca, rb, cb;
		cin >> ra >> ca >> rb >> cb;
		-- ra; -- rb;
		
		if (ca > cb) {
			swap(ra, rb);
			swap(ca, cb);
		}
		
		if (s[0] == 'O') {
			if (ca == cb) {
				valc[ca] = 1;
			}
			else {
				valr[rb][cb] = 1;
			}
			update(cb, 1);
		}
		
		else if (s[0] == 'C'){
			if (ca == cb) {
				valc[ca] = 0;
			}
			else {
				valr[rb][cb] = 0;
			}
			update(cb, 1);
		}
		
		else {
			node tmpl = query(1, ca, 1);
			node tmp = query(ca, cb, 1);
			node tmpr = query(cb, n, 1);
			
			if (tmp.cnt[ra][rb]) {
				cout << "Y" << "\n";
			}
			else if (tmp.cnt[ra][rb ^ 1] && tmpr.ra[0]) {
				cout << "Y" << "\n";
			}
			else if (tmp.cnt[ra ^ 1][rb] && tmpl.ra[1]) {
				cout << "Y" << "\n";
			}
			else if (tmp.cnt[ra ^ 1][rb ^ 1] && tmpl.ra[1] && tmpr.ra[0]) {
				cout << "Y" << "\n";
			}
			else {
				cout << "N" << "\n";
			}
		}
	}
	
	return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到