The Only Way to the Destination

The 9th CCPC (Harbin) Onsite(The 2nd Universal Cup. Stage 10: Harbin)
G. The Only Way to the Destination

题意:

有一个迷宫可以用 n × m n \times m n×m 个网格来表示。为了方便起见,我们将 ( x , y ) (x,y) (x,y) 定义为 x x x 行和 y y y 列中的网格。

最初,每个网格都是空的。爱丽丝想要设计这个迷宫。因此,她将在网格中放置 k k k 面墙。每面墙表示为 x 1 , x 2 , y x_1,x_2,y x1,x2,y ,这意味着 ∀ x 1 ≤ i ≤ x 2 \forall x_1\le i \le x_2 x1ix2 ( i , y ) (i, y) (i,y) 将成为这面墙的一部分,无法通过。除了墙格,其他网格都是空的。爱丽丝确保在放置 k k k 面墙后,所有空网格都保持连接,迷宫中至少有一个空网格。并且保证不同的墙没有共同的网格。

现在,爱丽丝想知道,是否有任何一对空网格之间只有一条简单路径相连?

如果您不熟悉 "简单路径 "的定义,请看这里:

连接空网格 ( x s , y s ) (x_s,y_s) (xs,ys) ( x d , y d ) (x_d,y_d) (xd,yd) 的简单路径定义为满足以下条件的网格位置 S = { ( x 1 , y 1 ) , ( x 2 , y 2 ) ⋯ ( x l e n , y l e n ) } , ( l e n ≥ 2 ) S=\{(x_1,y_1),(x_2,y_2)\cdots (x_{len},y_{len})\},(len\ge 2) S={(x1,y1),(x2,y2)(xlen,ylen)},(len2) 序列:

  • ( x 1 , y 1 ) = ( x s , y s ) , ( x l e n , y l e n ) = ( x d , y d ) (x_1,y_1)=(x_s,y_s),(x_{len},y_{len})=(x_d,y_d) (x1,y1)=(xs,ys),(xlen,ylen)=(xd,yd)
  • ∀ 1 ≤ i ≤ l e n ,   1 ≤ x i ≤ n , 1 ≤ y i ≤ m \forall 1\le i \le len,\ 1\le x_i \le n,1\le y_i\le m ∀1ilen, 1xin,1yim
  • ∀ 1 ≤ i ≤ l e n \forall 1\le i \le len ∀1ilen 是空网格
  • ∀ 1 ≤ i ≤ l e n − 1 ,   ∣ x i − x i + 1 ∣ + ∣ y i − y i + 1 ∣ = 1 \forall 1\le i \le len-1,\ |x_i-x_{i+1}|+|y_i-y_{i+1}| = 1 ∀1ilen1, xixi+1+yiyi+1=1
  • ∀ 1 ≤ i < j ≤ l e n , ( x i , y i ) ≠ ( x j , y j ) \forall 1\le i < j\le len, (x_i,y_i)\neq (x_j,y_j) ∀1i<jlen,(xi,yi)=(xj,yj)

如果任意两个不同的空网格 ( x s , y s ) , ( x d , y d ) , { ( x s , y s ) ≠ ( x d , y d ) } (x_s,y_s),(x_d,y_d),\{ (x_s,y_s)\ne (x_d,y_d)\} (xs,ys),(xd,yd),{(xs,ys)=(xd,yd)} 之间正好有一条简单路径相连,则输出 “是”。否则,输出 “否”。

输入:

第一行包含三个整数 n , m , k ( 1 ≤ n , m ≤ 1 0 9 , 1 ≤ k ≤ 1 0 5 ) n, m, k(1\le n, m \le 10^9, 1\le k \le 10^5) n,m,k(1n,m109,1k105) ,分别表示行数、列数和墙数。

接下来的 k k k 行,每行包含三个整数 x 1 , x 2 , y ( 1 ≤ x 1 ≤ x 2 ≤ n , 1 ≤ y ≤ m ) x_1,x_2,y(1\le x_1\le x_2\le n, 1\le y\le m) x1,x2,y(1x1x2n,1ym) ,表示迷宫中放置的墙。

保证每对空格之间至少有一条简单路径相连。

思路1:

好题,很难,这就是真题的含金量吗。

题意中给出了几个很重要的信息:(英语不行就怕碰到这种题)

  1. 墙只有纵向
  2. 爱丽丝确保在放置 k k k 面墙后,所有空网格都保持连接,迷宫中至少有一个空网格。并且保证不同的墙没有共同的网格
  3. 保证每对空格之间至少有一条简单路径相连

朴素想法是bfs,但是 1 ≤ n , m ≤ 1 0 9 1\le n, m \le 10^9 1n,m109 直接想都不要想了。想想看如果有多条简单路径时会怎么样。

首先对两列,如果左边连续的空网格和右边连续的空网格结合在一起,那么肯定不行,比如第三个样例,第二列两个连续的空网格和第三列两个连续的空网格形成了一个2*2的空格,这时不管其他地方怎么样,一定不可能满足条件了。

因此如果 n > 1 n>1 n>1 时,每两列必须要有一堵墙,否则两个空列接在一起一定不可能满足条件,这样的话 m m m 最多只有 2 k + 1 2k+1 2k+1,否则一定不可能满足条件。而 n = 1 n=1 n=1 时由于一定会有一条简单路径,而且也不可能出现其他简单路径,这时一定满足条件,特判直接返回即可。

其次,如果有多条简单路径,还不是连续网格相接,那么一定是环造成的。一个最简单的环就是一个矩形,这时它两边会有两个竖线(也就是连续空网格)将上下两条线相连,从左到右画的话,在连上后面的竖线时,就会导致成环,也就是把两个联通的部分又连接起来了,而成环的话,就必须有一条竖线把联通部分再相连。因此我们只用看后面的这段连续的空网格是否将多个联通的部分连接起来即可。

我们可以从左到右枚举列,对每一列,从上到下枚举连续空网格区间,看他是否将前面多个联通的部分再次相连。也就是枚举前面一列对应的区间内的空网格区间,判断两两是否联通,如果有联通,就说明有环,答案就是NO,否则将它们联通起来。

如何判断联通:

  • 如果前面一列没区间,说明这一块是凭空产生的,分配一个颜色
  • 有区间的话如果不连通的颜色都唯一,这时这几种颜色联通,并查集维护一致性
  • 如果某个联通的颜色不唯一,说明无解

code1:

有个函数没写返回值爆吃16个 RE 41,血书

由于最多会产生 m + k m+k m+k 段空网格区间, 而且 m ≤ 2 k + 1 m\le 2k+1 m2k+1,所以并查集最多开个 3 k 3k 3k 就很够用了。实际上如果要满足条件,空集合个数最多不会超过 2 k 2k 2k

找前一列对应的区间内的空网格区间用二分是最保险的,最坏情况下不会超时,但是太难写了,先写个枚举跑一下,结果过了。(也就是 f i n d l ( ) findl() findl() f i n d r ( ) findr() findr() 函数)

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;

int n,m,k;
struct wall{
	int x1,x2,y;
	bool operator<(wall x){
		if(y!=x.y)return y<x.y;
		return x1<x.x1;
	}
}w[maxn];

struct interval{
	int l,r,color;
}; 
vector<interval> lst,cur;//空区间左端点 右端点 

int c,f[maxn<<1];
int getf(int x){
	if(f[x]==x)return x;
	else return f[x]=getf(f[x]);
}
void merge(int a,int b){
	f[getf(b)]=getf(a);//把b合并到a上 
}

int findl(int l){
	for(int i=0;i<lst.size();i++){
		if(lst[i].r>=l)return i;
	}
	return maxn<<5;
}
int findr(int r){
	for(int i=lst.size()-1;i>=0;i--){
		if(lst[i].l<=r)return i;
	}
	return -1;
}

bool solve(){
	if(n==1)return true; 
	for(int i=1;i<(maxn<<1);i++)f[i]=i;
	
	for(int col=1,i=0,lstr;col<=m;col++){
		lstr=0;//上一个墙的右端点 下一段空区间为[lstr+1,w[].x1-1] 
		cur.clear();
		while(i+1<=k && w[i+1].y==col){
			i++;
			if(lstr+1<=w[i].x1-1)
				cur.push_back((interval){lstr+1,w[i].x1-1,-1});//中间有缝隙 
			lstr=w[i].x2;
		}
		if(lstr+1<=n)cur.push_back((interval){lstr+1,n,-1});
		
		
		int l,r,lidx,ridx,lstf;//枚举空区间,左右端点,前一个左右区间 
		for(auto &tmp:cur){
			l=tmp.l;
			r=tmp.r;
			lidx=findl(l);
			ridx=findr(r);
			lstf=-1;//所属颜色 
			
			for(int i=lidx,tl,tr;i<=ridx;i++){
				tl=max(l,lst[i].l);
				tr=min(r,lst[i].r);
				if(tr-tl>=1)return false;
				if(~lstf){
					if(getf(lst[i].color)!=lstf)
						merge(lstf,lst[i].color);
					else return false;//两个颜色已经联通 
				}
				else lstf=getf(lst[i].color);
			}
			
			if(~lstf)tmp.color=lstf;
			else tmp.color=++c;
		}
		lst=cur;
	}
	return true;
}

int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++)
		cin>>w[i].x1>>w[i].x2>>w[i].y;
	sort(w+1,w+k+1);
	
	puts(solve()?"YES":"NO");
	
	return 0;
}

思路2:

和上面枚举连续空网格区间,一个一个处理的做法是一样的,换句话说,其实上面就是把区间当作一个点来对待了,相当于缩点。前一列的点和后一列的点如果有交集,就说明这两个点之间连有一条边,表示从两个点通过这个边可以相互到达。

因为保证了所有空网格联通,所以所有点一定是联通的。假设缩出了 c n t cnt cnt 个点,如果只有一条简单路径,那么形成的就是有 c n t cnt cnt 个点和 c n t − 1 cnt-1 cnt1 条边的树。而有多条简单路径就意味着有 c n t cnt cnt 个点和 > c n t − 1 \gt cnt-1 >cnt1 个边的路径。

所以我们一边缩点,一边统计点和边的个数,最后看一下是不是 c n t cnt cnt 个点和 c n t − 1 cnt-1 cnt1 条边就行了。就没必要并查集判断联通了。

code2:

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;

int n,m,k;
struct wall{
	int x1,x2,y;
	bool operator<(wall x){
		if(y!=x.y)return y<x.y;
		return x1<x.x1;
	}
}w[maxn];

struct interval{int l,r,color;}; 
vector<interval> lst,cur;//空区间左端点 右端点 

int findl(int l){
	for(int i=0;i<lst.size();i++){
		if(lst[i].r>=l)return i;
	}
	return maxn<<5;
}
int findr(int r){
	for(int i=lst.size()-1;i>=0;i--){
		if(lst[i].l<=r)return i;
	}
	return -1;
}

int nd,eg;
bool solve(){
	if(n==1)return true;
	nd=eg=0;
	
	for(int col=1,i=0,lstr;col<=m;col++){
		lstr=0;//上一个墙的右端点 下一段空区间为[lstr+1,w[].x1-1] 
		cur.clear();
		while(i+1<=k && w[i+1].y==col){
			i++;
			if(lstr+1<=w[i].x1-1)
				cur.push_back((interval){lstr+1,w[i].x1-1,-1});//中间有缝隙 
			lstr=w[i].x2;
		}
		if(lstr+1<=n)cur.push_back((interval){lstr+1,n,-1});
		
		
		int l,r,lidx,ridx,lstf;//枚举空区间,左右端点,前一个左右区间 
		for(auto &tmp:cur){
			nd++;
			l=tmp.l;
			r=tmp.r;
			lidx=findl(l);
			ridx=findr(r);
			
			for(int i=lidx,tl,tr;i<=ridx;i++){
				tl=max(l,lst[i].l);
				tr=min(r,lst[i].r);
				if(tr!=tl)return false;
				eg++;
			}
		}
		lst=cur;
	}
	return eg+1==nd;
}

int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++)
		cin>>w[i].x1>>w[i].x2>>w[i].y;
	sort(w+1,w+k+1);
	
	puts(solve()?"YES":"NO");
	
	return 0;
}

更新:

思路3:

学了珂朵莉树和颜色段均摊之后,发现上面的写法和珂朵莉树的区间推平操作 a s s i g n ( ) assign() assign() 函数很像,可以用颜色段均摊的思想来写。

如果我们把一列看作一段一段的颜色段,墙表示颜色0,空网格表示颜色1,我们用珂朵莉树维护出来了第 i i i 列的颜色段,现在要涂成下一列的颜色段,如果要涂的颜色是1,那么涂色之前查询一下这个区间内的所有颜色1的部分有几个,如果其中有宽度大于1的直接输出 “NO”。

根据上面的思路2,我们把一列上的一块颜色1区间看作是一个点,和前面区间内的颜色1区间连边,那么这个树的节点数应该是边数+1。所以我们涂一个颜色1区间次数就是点的个数,查询时查到的区间1的个数累加起来就是边的个数。最后看一眼 点=边+1 就行了。

写下来就只有一个珂朵莉树板子,很板。

code:

#include <iostream>
#include <cstdio>
#include <set>
#include <algorithm> 
#include <vector>
using namespace std;
const int maxn=1e5+5;
#define pii pair<int,int>

int n,m,k;

struct ODT{
	#define SIT set<Node>::iterator
	#define type bool
	
	struct Node{
		int l,r;
		mutable type val;
		Node(int l,int r=0,type val=0):l(l),r(r),val(val){};
		bool operator<(const Node x)const{return l<x.l;}
	};
	set<Node> s;
	
	void build(int n){
		s.insert(Node(1,n,0));
		s.insert(Node(n+1,n+1,0));
		return;
	}
	void print(){
		for(auto x:s)
			printf("[%d,%d] %d\n",x.l,x.r,x.val);
		puts("");
	}
	SIT split(int pos){
		SIT it=s.lower_bound(Node(pos));
		if(it!=s.end() && it->l==pos)return it;
		it--;
		int l=it->l,r=it->r;
		type val=it->val;
		s.erase(it);
		s.insert(Node(l,pos-1,val));
		return s.insert(Node(pos,r,val)).first;
	}
	void assign(int l,int r,type v){
		SIT it2=split(r+1),it1=split(l);
		s.erase(it1,it2);
		s.insert(Node(l,r,v));
		return;
//		cout<<"assign:"<<l<<" "<<r<<" "<<v<<endl;
	}
	int count(int l,int r){//数一下[l,r]里宽度为1的点 宽度超过1则返回-1 
		SIT it2=split(r+1),it1=split(l);
		int cnt=0;
		for(;it1!=it2;it1++)
			if(it1->val){
				if(it1->r-it1->l>0){
					puts("NO");
					exit(0);
				}
				cnt++; 
			}
		return cnt;
	}
	
	#undef SIT
	#undef type
}tr;

vector<pii> a[maxn<<1];

int main(){
	cin>>n>>m>>k;
	if(n==1){
		puts("YES");
		return 0;
	}
	if(m>2*k+1){
		puts("NO");
		return 0;
	}
	tr.build(n);
	for(int i=1,x1,x2,y;i<=k;i++){
		cin>>x1>>x2>>y;
		a[y].push_back(pii(x1,x2));
	}
	
	int nd=0,eg=0;
	for(int col=1,lst;col<=m;col++){
		lst=1;
		sort(a[col].begin(),a[col].end());
		for(auto x:a[col]){
			int l=x.first,r=x.second;
			if(lst<l){
				nd++;
				eg+=tr.count(lst,l-1);
				tr.assign(lst,l-1,1);
			}
			tr.assign(l,r,0);
			lst=r+1;
		}
		if(lst<=n){
			nd++;
			eg+=tr.count(lst,n);
			tr.assign(lst,n,1);
		}
	}
	puts((nd==eg+1)?"YES":"NO");
	return 0;
}
  • 47
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值