[CF1503F]Balance the Cards

题目

传送门 to CF

题目概要
一种 “数字括号序列” 的定义如下:

  • 空串是 “数字括号序列” 。
  • 如果 A , B A,B A,B 都是 数字括号序列,那么二者的拼接 [ A , B ] [A,B] [A,B] 也是。
  • 如果 A A A数字括号序列,那么在首尾加入一对相反数 [ x , A , − x ] [x,A,-x] [x,A,x] 也是。注意 x ∈ N + x\in{\Bbb N}^+ xN+

现在有 2 n 2n 2n 张卡片,正面的数字集合恰好是 { x ∣ x 2 ≤ n 2 ∧ x ≠ 0 } \{x\mid x^2\le n^2\wedge x\ne 0\} {xx2n2x=0},反面的数字集合也是它。但是不保证一张卡片上的两个数字有某种关联。

现在,请重新排列所有的卡片,使得正面的数字、反面的数字分别构成 数字括号序列,输出方案,或者报告无解。再次提醒,将卡牌翻面是不被允许的哦!

数据范围与提示
n ≤ 2 × 1 0 5 n\le 2\times 10^5 n2×105

思路

事实上,你会发现 x    ( x ∈ N + ) x\;(x\in{\Bbb N}^+) x(xN+) 只能和 − x -x x 匹配。这是一个特异性匹配。更强的是,二者的先后关系也是确定的。

不难想到,把两个可以匹配的卡片连边。将卡片横着排一行,上面连正面对应的边,下面连反面对应的边。问题转化为,怎么样重新排列卡片,使得 x x x − x -x x 前,且 图是一个平面图(边之间不相交)。剽窃一个官方题解的图:

盗图大师就是我
由于每个卡片恰好连了两条边,新的图肯定是很多环构成的。对应到平面图上,就是很多平滑曲线。

用一点平面几何的知识——任意多边形外角和为 2 π 2\pi 2π 。此处的平滑曲线( J o r d e n    C u r v e \rm Jorden\;Curve JordenCurve)利用微分的思想,其 “外角和”(本质是旋转的角度)也是 2 π 2\pi 2π 。随便规定一个正方向,比如顺时针,那么 “顺时针边” 比 “逆时针边” 恰好多两条。

如果从最左边的点出发,那么第一条边和最后一条边都一定是 “顺时针边” 。比如上面那个图,从最左边的点出发,先走上方那条边,用 1 1 1 代表 “顺时针边”,我们将得到 1 , 0 , 1 , 1 , 0 , 1 1,0,1,1,0,1 1,0,1,1,0,1

说了这么多,感觉没用啊……因为卡片要重新排列……然而,有连边的卡片之间,前后关系是确定的!所以说,边是 “顺时针” 或 “逆时针” 是确定的

你认为我们不知道起点,所以就无法确定了吗?诚然,如果从第二个点出发,访问顺序会颠倒。但是我们利用 “顺时针边” 数量更多这一点,如果 “顺时针边” 少了,那就反转(将访问顺序翻转,那么边的 “旋转方向” 就会反转)。

找起点也并不是很难。我们只需要找到相邻的 “顺时针边”,并把这个公共端点设置为起点就行了。

两个连通块之间互不干扰(比如上面那个图,完全可以把蓝色的环放在外面),所以我们面临的问题变成了:

已知 k k k 条边的 “旋转方向”,找一个方法使得边之间不相交(构成平面图)。

我们肯定会想办法 递归。为了方便,仍然用前面的 0 / 1 0/1 0/1 表示法,以最左边的点为起点,顺时针走。由于首尾必然都是 1 1 1,中间的必然 0 , 1 0,1 0,1 等量。那么我们会想到,把中间的串拆成两个 0 , 1 0,1 0,1 等量的串,然后拼接。形式化地说:

  • 如果串是 [ 1 , s , t , 1 ] [1,s,t,1] [1,s,t,1] 满足 s , t s,t s,t 0 , 1 0,1 0,1 等量,那么先构造出 [ 1 , s , 1 ] [1,s,1] [1,s,1] [ 1 , t , 1 ] [1,t,1] [1,t,1],然后直接让 s s s 中的最后一个点和 t t t 中的第一个点合并即可。

为啥是正确的呢?因为 s s s 中的最后一个点,正准备走一个下方的边回家,它的左右两边都有很多空间,直接把 t t t 填进去就可以了。

但是有没有可能无法划分呢?显然 [ 1 , 1 , ? , 1 , 1 ] [1,1,?,1,1] [1,1,?,1,1] [ 1 , 0 , ? , 0 , 1 ] [1,0,?,0,1] [1,0,?,0,1] 都是必然可以划分的。那么我们特殊处理一下 [ 1 , 1 , s , 0 , 1 ] [1,1,s,0,1] [1,1,s,0,1] [ 1 , 0 , s , 1 , 1 ] [1,0,s,1,1] [1,0,s,1,1] 好了。

  • 如果串是 [ 1 , 1 , s , 0 , 1 ] [1,1,s,0,1] [1,1,s,0,1],先构造 [ 1 , s ˉ , 1 ] [1,\bar s,1] [1,sˉ,1],然后将 s s s 对应的部分翻转得到 s ˉ \bar s sˉ ,在 s ˉ \bar s sˉ 前后分别接入点即可。
  • 如果串是 [ 1 , 0 , s , 1 , 1 ] [1,0,s,1,1] [1,0,s,1,1] 同理。都需要将 [ 1 , s , 1 ] [1,s,1] [1,s,1] 的结果进行翻转。

递归出口当然是 s = ø s=\text{\o} s=ø,此时原串就是 [ 1 , 1 ] [1,1] [1,1],就是两个点直接连成一个环呗。

如何实现以上过程?

  • 翻转怎么做?妹儿湘会告诉你平衡树。我们用两个链表,一个存正序,一个存逆序,翻转就是 s w a p \rm swap swap 呗。交换指针当然是 O ( 1 ) \mathcal O(1) O(1) 的啦!
  • 然后是找到分界点。这里是个 c o m m o n    t r i c k \rm common\;trick commontrick 了,两边同时往中间找。这是启发式合并的复杂度。
  • 答案也得用链表存。拼接恰好是 O ( 1 ) \mathcal O(1) O(1) 的。

于是我们可以在 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 的时间复杂度内解决它了。事实上,我们还有一个 O ( n ) \mathcal O(n) O(n) 的做法:维护一个栈,定义 Δ \Delta Δ 为前缀 1 1 1 的数量与前缀 0 0 0 的数量的差(取绝对值),当 Δ \Delta Δ 变大时,往栈里放一个 [ 1 , 1 ] [1,1] [1,1],当 Δ \Delta Δ 变小时,对栈顶元素进行左右加 1 , 0 1,0 1,0 0 , 1 0,1 0,1(视情况而定),然后将它与下一个栈顶合并。(事实上这一段不是很好描述,建议看代码)

上面这个做法又为什么是正确的呢?其实就是把序列划分成很多 0 , 1 0,1 0,1 等量的串。划分过程类似于括号匹配——恰好匹配的时候也就做了 [ 1 , 0 / 1 , s , 1 / 0 , 1 ] [1,0/1,s,1/0,1] [1,0/1,s,1/0,1] 的一个操作。

代码

干脆拆解成小部分,分别讲解好了。

找路径

找路径很简单,走一条上面的边、一条下面的边,最后走回去就行。

		/* get the path */ ;
		int x = i, cnt = 0;
		while(!vis[x]){
			vis[x] = true;
			path.push_back(x);
			cnt += (a[x] > 0);
			s.push_back('0'+(a[x]>0));
			if(a[x] < 0)
				x = pos[0][0][-a[x]];
			else x = pos[0][1][a[x]];

			vis[x] = true;
			path.push_back(x);
			cnt += (b[x] < 0);
			s.push_back('0'+(b[x]<0));
			if(b[x] < 0)
				x = pos[1][0][-b[x]];
			else x = pos[1][1][b[x]];
		}
调整路径

然后需要调整路径:翻转。然而翻转 p a t h path path 挺麻烦,可以将最后得到的答案翻转。

		/* adjust order */ ;
		if(cnt < int(s.length()>>1)){
			int len = s.length();
			cnt = len-cnt; // flip
			rep(i,0,len-1)
				s[i] = '0'+'1'-s[i];
		}
找起点

然后是找起点。找到之后,直接把 s s s p a t h path path 进行对应的调整,使得起点是第 2 2 2 个点(也就是新的 p a t h path path 1 1 1 下标对应元素)。

		int idx = 0;
		for(; s[idx]=='0'||s[idx+1]=='0'; ++idx);
		rotate(s.begin(),s.begin()+idx,s.end());
		rotate(path.begin(),path.begin()+idx,path.end());
初状态

注意,这里并不是为了避免 R E \rm RE RE,它是真正有用的——最后一个点一定是起点 p a t h [ 1 ] path[1] path[1] 的前一个点, p a t h [ 0 ] path[0] path[0]

		sta.clear(); stnode.push_back(new Curve(0));

接下来从右往左扫描 s s s,依次处理。

情况一

如果这是一个 “左括号”,那么加入一个空 c u r v e curve curve,表示 不含有目前的左括号的方案,同样的,栈里面加入一个 “左括号” 。(所以说,在 操作二 中,需要把 “左括号” 拿出来一起拼接上去。)

			if(sta.empty() || s[sta.back()] == s[j]){
				sta.push_back(j);
				stnode.push_back(new Curve());
			}
操作二

干脆讲讲为啥要合并两个段。现在得到的就是形如 [ 1 , ( , s , t , ? , 1 ] [1,(,s,t,?,1] [1,(,s,t,?,1] 的形式,其中 s , t s,t s,t 0 , 1 0,1 0,1 等量的。 t t t 就是刚刚因为括号完美匹配而得到的玩意儿。那么就把 s , t s,t s,t 合起来就行了。反正最后会有一个 ? ? ? ) ) ) 来封口的。

至于究竟是 [ 1 , s , 0 ] [1,s,0] [1,s,0] 还是 [ 0 , s , 1 ] [0,s,1] [0,s,1],这些都是小细节,我就略过了。

输出答案

最后一个点是特殊加入栈里的,第一个点却被我们忽视了。——我们求的就是 [ 1 , s , 1 ] [1,s,1] [1,s,1] s s s 对应的那一部分点,只不过最后一个点提前塞进去了。——把第一个点放进去就行了。

完整代码

不会有人连 l i s t \sout{list} list都不会用吧?总不可能不会上 r e f e r e n c e \sout{reference} reference查吧

#include <bits/stdc++.h>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(int x){
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}

struct Curve{
	list<int> fw, bw;
	Curve(){
		fw.clear(); bw.clear();
	}
	Curve(int x){
		fw.push_back(x);
		bw.push_back(x);
	}
	Curve& operator += (Curve &t){
		fw.splice(fw.end(),t.fw);
		bw.swap(t.bw);
		bw.splice(bw.end(),t.bw);
		return *this;
	}
	Curve& reverse(){
		fw.swap(bw); return *this;
	}
};

const int MaxN = 400005;
int pos[2][2][MaxN]; // 0/1:front/back  0/1:open/close
int a[MaxN], b[MaxN];

bool vis[MaxN]; string s;
vector<int> path, sta, ans;
vector<Curve*> stnode;
int main(){
	int n = readint();
	for(int i=1; i<=(n<<1); ++i){
		a[i] = readint(), b[i] = readint();
		if(a[i] > 0) pos[0][0][a[i]] = i;
		else pos[0][1][-a[i]] = i;
		if(b[i] > 0) pos[1][0][b[i]] = i;
		else pos[1][1][-b[i]] = i;
	}
	for(int i=1; i<=(n<<1); ++i){
		if(vis[i]) continue;
		path.clear(), s.clear();
		/* get the path */ ;
		int x = i, cnt = 0;
		while(!vis[x]){
			vis[x] = true;
			path.push_back(x);
			cnt += (a[x] > 0);
			s.push_back('0'+(a[x]>0));
			if(a[x] < 0)
				x = pos[0][0][-a[x]];
			else x = pos[0][1][a[x]];

			vis[x] = true;
			path.push_back(x);
			cnt += (b[x] < 0);
			s.push_back('0'+(b[x]<0));
			if(b[x] < 0)
				x = pos[1][0][-b[x]];
			else x = pos[1][1][b[x]];
		}
		/* adjust order */ ;
		if(cnt < int(s.length()>>1)){
			int len = s.length();
			cnt = len-cnt; // flip
			rep(i,0,len-1)
				s[i] = '0'+'1'-s[i];
		}
		/* check constraint */ ;
		if(cnt != int(s.length()>>1)+1){
			puts("NO"); return 0;
		}
		/* find starting position */ ;
		int idx = 0;
		for(; s[idx]=='0'||s[idx+1]=='0'; ++idx);
		rotate(s.begin(),s.begin()+idx,s.end());
		rotate(path.begin(),path.begin()+idx,path.end());
		sta.clear(); stnode.push_back(new Curve(0));
		for(int j=int(s.length())-1; j>=2; --j)
			if(sta.empty() || s[sta.back()] == s[j]){
				sta.push_back(j);
				stnode.push_back(new Curve());
			}
			else{
				Curve *x = new Curve(j); // part I
				Curve *z = stnode.back(); // part II
				Curve *y = new Curve(sta.back()); // part III
				sta.pop_back(), stnode.pop_back();
				/* 0 + s + 1 */ ;
				if(s[j] == '0'){
					*x += *y; // inverted
					*x += z->reverse();
					*x += *stnode.back();
					stnode.pop_back(); // merge two
					stnode.push_back(x);
				}
				/* 1 + s + 0 */ ;
				if(s[j] == '1'){
					Curve *w = stnode.back();
					stnode.pop_back(); // merge two
					*w += z->reverse();
					*w += *y; *w += *x;
					stnode.push_back(w);
				}
			}
		Curve res = Curve(1);
		res += *stnode.back();
		if(a[path[1]] < 0)
			res.reverse();
		for(int sy : res.fw)
			ans.push_back(path[sy]);
		stnode.pop_back(); // last one
	}
	puts("YES");
	for(int sy : ans)
		printf("%d %d\n",a[sy],b[sy]);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值