Szkopul (rek). Task Recursive Ant

Posted Neal_lee

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Szkopul (rek). Task Recursive Ant相关的知识,希望对你有一定的参考价值。

题目链接

Szkopul Task Recursive Ant (rek)

题目大意

有一张 \\(2^n\\times2^n\\) 的网格图,其中有 \\(m\\) 的格子有障碍无法到达,从 \\((0,0)\\) 即左上角的格子开始,每次移动到相邻的一个无障碍且未被访问的格子,移动的路径有以下要求:

对于当前处在的一个 \\(2^k\\times2^k(k\\geq 1)\\) 的区块内,它由四个 \\(2^{k-1}\\times2^{k-1}\\) 更小的区块组成,你需要依次把每个块完整访问一遍,即进入一个块之后,需要把其内所有无障碍格子都走一遍,然后才可以离开这个区域。

现在问是否能够最终分别从网格图的四个边界离开网格图,若一个方向可以,则输出一个可行的最终离开格子的坐标,否则输出 NIE(波兰语的 NO)。输出的顺序是 ”上右下左“ 。

\\(0\\leq n\\leq 30\\), \\(0\\leq m\\leq 50\\)

思路

注意到 \\(m\\leq 50\\)\\(2^n\\leq 2^{30}\\) 的数量级差的很大,那应该就可以想到我们要对 \\(m\\) 个障碍有关的事物专门处理,而其它没有障碍的部分是可以很快地得到结果的。

先考虑如何快速地得到无障碍部分的结果。

如果直接从左上角进入,可以发现是从「从右上到左下的对角线」的两侧离开该区域的,这一点用数学归纳法的思路很容易进行严谨证明。注意左图中 \\(2\\times2\\) 的基础图形,它只有从上面一格开始走和从下面一格开始走两种情况,如果从 \\((1,0)\\) 进入,那就是从离对角线(后文的对角线都指之前定义的那个)距离 \\(1\\) 的格子离开,从而如果我们在左侧从任意一个位置进入网格图,这个位置可以进行类似于二进制拆解的思路,对应到基础情况中的从上面走还是从下面走,最终可以得出一个结论(此处可以手画一画,更明确的感知一下):当进入的位置离 \\((0,0)\\)\\(d\\) 时,我们最终会从离对角线 \\(d\\) 的地方(共 \\(4\\) 个)离开此区域。

所以我们知道进入的位置时,可以 \\(O(1)\\) 地得到它最终所有可以离开的位置。

 

而对于有障碍物的格子,它们好像很难有什么规律,那么我们对所有包含障碍物的区域都暴力计算即可,这样的区域数量是 \\(O(nm)\\) 级别的(最多嵌套 \\(n\\) 层)。于是我们可以递归处理原问题,对于当前 \\(2^k\\times2^k\\) 的块,如果它不包含障碍,直接 \\(O(1)\\) 返回所有可行的出口(根据之前的分析可以知道这个出口数量 \\(\\leq 4\\)),否则分别递归 \\(4\\)\\(2^{k-1}\\times2^{k-1}\\) 的块,将块块之间组合,枚举两种遍历方式(即遍历 \\(2\\times2\\) 可以有的那两种),即可以得到当前块的答案了。

时间复杂度:\\(O(nm)\\)

 

实现

上面的思路说的轻巧,但这个东西实现起来还挺难的,因此专门讲讲怎么实现。

我们需要实现一个函数 solve(x, y, len, k),目前在考虑左上角坐标为 \\((x,y)\\),边长为 \\(len\\) 的区域,含有障碍的区域其规律很难把控,我们只求当输入为 \\(k\\) 时它最终可以离开的格子位置,称其为输出。

输入和输出都是在边界的地方进行的,由之前的分析可以知道它们与「从右上到左下的对角线」有关,这里定义一个结构体 struct io{ int dir, dis; }; 表示这个输入/输出位置的方向为 dir,顺序与最终输出答案的方向顺序相同,这个位置离对角线的距离为 dis,为了方便这里 dis 的范围是 \\([1,2^k]\\)

solve 函数要返回一系列输出,所以输出类型为 vector<io> ,若此区域无障碍,直接计算结果即可。

bool exist(int x0, int y0, int x1, int y1){
	rep(i,0,m-1)
		if(x[i] >= x0 && x[i] <= x1 && y[i] >= y0 && y[i] <= y1)
			return true;
	return false;
}
....
vector<io> ret;
if(!exist(x, y, x+len-1, y+len-1)){
	rep(i,0,3) ret.push_back({i, len-k.dis+1});
	return ret;
}

若此区域全被障碍填满了,那也不需要在讨论了,直接返回空 vector。

bool filled(int a, int b, int len){
	if((len >= 8) || len*len > m) return false;
	int cnt = 0;
	rep(i,0,m-1)
		if(x[i] >= a && x[i] < a+len && y[i] >= b && y[i] < b+len) cnt++;
	return cnt == len*len;
}
...
if(filled(x, y, len)) return {};

注意 filled 函数要判一下 len 是否 \\(\\geq 8\\),否则直接相乘可能会 int 溢出。

接下处理一般性的情况,这里我们有两种行走的方式,而不同的方式里小方块之间输入输出不相同,还与传进来的输入有关,如果直接分类讨论,那么可能会非常复杂。考虑这里再实现一个函数 go(x, y, dir, len, k),表示从 \\((x,y,len)\\) 的那个方块开始,k 为它的初始输入,按照 dir 的方向进行一系列游走,这里 dir 是一个 vector<int>,在此情况最终的所有输出。

先假设 go 函数已经写好了,这里我们再讨论一下当前是从那个方位的方块开始的,然后把 go 函数两种游走方式下的输出合并即可,由于这里合并要去重,我实现了一个 map<io, bool>,并为 io 定义了大小比较关系:

struct io{ 
	int dir, dis;
	bool operator < (const io b) const{ 
		if(dir != b.dir) return dir < b.dir;
		return dis < b.dis;
	}
};
map<io, bool> vis;
....
    
vector<io> a, b;
if((k.dir == 0 || k.dir == 3) && k.dis > len/2){
	k.dis -= len/2;
	a = go(x, y, len/2, {1, 2, 3}, k), b = go(x, y, len/2, {2, 1, 0}, k);
}
else if((k.dir == 0 || k.dir == 1) && k.dis <= len/2)
	a = go(x, y+len/2, len/2, {3, 2, 1}, k), b = go(x, y+len/2, len/2, {2, 3, 0}, k);
else if((k.dir == 1 || k.dir == 2) && k.dis > len/2){
	k.dis -= len/2;
	a = go(x+len/2, y+len/2, len/2, {3, 0, 1}, k), b = go(x+len/2, y+len/2, len/2, {0, 3, 2}, k);
}
else if((k.dir == 2 || k.dir == 3) && k.dis <= len/2)
	a = go(x+len/2, y, len/2, {0, 1, 2}, k), b = go(x+len/2, y, len/2, {1, 0, 3}, k);

vis.clear();
for(io p : a) if(!vis[p]) ret.push_back(p), vis[p] = true;
for(io p : b) if(!vis[p]) ret.push_back(p), vis[p] = true;

 

接下来考虑实现 go(x, y, dir, len, k) ,这里有一个麻烦的地方,就是当这 \\(4\\) 个方块中有一些被完全填上的时候,我们是不用走它的,这使最终的输出的方向并不好确定,我还需要知道最后一个走的块的方位(\\(2\\times2\\))是什么。

于是我们先处理出这个起始方块的方位,这一点可以通过 dir 还原出来,看前两步是咋走的即可。

int X[4] = {-1, 0, 1, 0}, Y[4] = {0, 1, 0, -1};
...
int a = 0, b = 0;
rep(i,0,1) a += X[dir[i]], b += Y[dir[i]];
a = (a < 0), b = (b < 0);

然后我们一步步去走,每次把上一步的输出转化成输出传给下一个块的 solve,然后根据游走实际所需的方向,保留那个需要的输出,如果没有则说明此次游走是不可能达成的,直接返回空 vector。而如果发现下一个块被障碍完全填满了,这意味着我们的行程就要到此为止了,检查一下再后面的方块是否也被填满了,没填满(那我们也没法访问了)就返回空 vector。

\\((a,b)\\) 实时记录当前块的方位,最后需要根据这个实际方位去找合适的输出,这个合适的输出为了避免分类讨论,这里用数组预处理出来

int ch[2][2][2] = {
	{{0, 3}, {0, 1}},
	{{2, 3}, {1, 2}}
};

前两位是方位 \\((a,b)\\),第三维放两种可行的方向。

注意最后返回输出的时候,输出要转化成大方块的格式,所以还需根据方位的确定 dis 是否需要加上一个 len 。最终 go 函数的实现如下:

vector<io> go(int x, int y, int len, vector<int> dir, io k){
	int a = 0, b = 0;
	rep(i,0,1) a += X[dir[i]], b += Y[dir[i]];
	a = (a < 0), b = (b < 0);

	rep(i,0,2){
		vector<io> tmp = solve(x, y, len, k);
		bool flag = false;
		io tmpk = k;
		for(io p : tmp) if(p.dir == dir[i]) k = p, flag = true;
		if(!flag) return {};

		if(k.dir >= 2) k.dir -= 2;
		else k.dir += 2;
		k.dis = len-k.dis+1;
		x += X[dir[i]]*len, y += Y[dir[i]]*len;
		a += X[dir[i]], b += Y[dir[i]];

		if(filled(x, y, len)){
			int tmpx = x-X[dir[i]]*len, tmpy = y-Y[dir[i]]*len;
			rep(j,i+1,2){
				x += X[dir[j]]*len, y += Y[dir[j]]*len;
				if(!filled(x, y, len)) return {};
			}
			x = tmpx, y = tmpy, k = tmpk;
			a -= X[dir[i]], b -= Y[dir[i]];
			break;
		}
	}
	vector<io> tmp = solve(x, y, len, k), ret;
	for(io p : tmp)
		if(p.dir == ch[a][b][0] || p.dir == ch[a][b][1])
			ret.push_back(p);
	int id = a^b^1;
	rep(i,0,(int)ret.size()-1) ret[i].dis += id*len;
	return ret;
}

 

然而事情还没有结束,回到 solve 函数,由于有大量二选一的操作,边长为 \\(2^k\\) 的块下面最差要做 \\(2^k\\) 次决策,但是它们中只有 \\(O(1)\\) 不同的情况(因为只有方向会有变化,相对对角线的位置是不变的),对 solve 的操作进行记忆化,记 struct state{ int a, b, c; io d; } 为一个状态,给它定义大小比较之后塞 map 里面即可。

然后基本上就可以得到完整的代码了,由于要 map 记忆化,实际实现的时间复杂度是 \\(O(nm\\log(nm))\\) 的,把它换成哈希就可以达到理论复杂度了。

 

Code

#include<iostream>
#include<vector>
#include<cstring>
#include<map>
#define mem(a,b) memset(a, b, sizeof(a))
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 55
using namespace std;

int n, m;
int x[N], y[N];
int X[4] = {-1, 0, 1, 0}, Y[4] = {0, 1, 0, -1};
int ch[2][2][2] = {
	{{0, 3}, {0, 1}},
	{{2, 3}, {1, 2}}
};
int end_x[4], end_y[4];

struct io{ 
	int dir, dis;
	bool operator < (const io b) const{ 
		if(dir != b.dir) return dir < b.dir;
		return dis < b.dis;
	}
};
struct state{
	int a, b, c;
	io d;
	bool operator < (const state bb) const{ 
		if(d < bb.d || bb.d < d) return d < bb.d;
		if(a != bb.a) return a < bb.a;
		if(b != bb.b) return b < bb.b;
		return c < bb.c;
	}
};
map<io, bool> vis;
map<state, vector<io> > f;

bool exist(int x0, int y0, int x1, int y1){
	rep(i,0,m-1)
		if(x[i] >= x0 && x[i] <= x1 && y[i] >= y0 && y[i] <= y1)
			return true;
	return false;
}
bool filled(int a, int b, int len){
	if((len >= 8) || len*len > m) return false;
	int cnt = 0;
	rep(i,0,m-1)
		if(x[i] >= a && x[i] < a+len && y[i] >= b && y[i] < b+len) cnt++;
	return cnt == len*len;
}

vector<io> solve(int x, int y, int len, io k);

vector<io> go(int x, int y, int len, vector<int> dir, io k){
	int a = 0, b = 0;
	rep(i,0,1) a += X[dir[i]], b += Y[dir[i]];
	a = (a < 0), b = (b < 0);

	rep(i,0,2){
		vector<io> tmp = solve(x, y, len, k);
		bool flag = false;
		io tmpk = k;
		for(io p : tmp) if(p.dir == dir[i]) k = p, flag = true;
		if(!flag) return {};

		if(k.dir >= 2) k.dir -= 2;
		else k.dir += 2;
		k.dis = len-k.dis+1;
		x += X[dir[i]]*len, y += Y[dir[i]]*len;
		a += X[dir[i]], b += Y[dir[i]];

		if(filled(x, y, len)){
			int tmpx = x-X[dir[i]]*len, tmpy = y-Y[dir[i]]*len;
			rep(j,i+1,2){
				x += X[dir[j]]*len, y += Y[dir[j]]*len;
				if(!filled(x, y, len)) return {};
			}
			x = tmpx, y = tmpy, k = tmpk;
			a -= X[dir[i]], b -= Y[dir[i]];
			break;
		}
	}
	vector<io> tmp = solve(x, y, len, k), ret;
	for(io p : tmp)
		if(p.dir == ch[a][b][0] || p.dir == ch[a][b][1])
			ret.push_back(p);
	int id = a^b^1;
	rep(i,0,(int)ret.size()-1) ret[i].dis += id*len;
	return ret;
}

vector<io> solve(int x, int y, int len, io k){
	vector<io> ret, a, b;
	if(!exist(x, y, x+len-1, y+len-1)){
		rep(i,0,3) ret.push_back({i, len-k.dis+1});
		return ret;
	}
	if(filled(x, y, len)) return {};
	if(f.count({x, y, len, k})) return f[{x, y, len, k}];

	int tmp = k.dis;
	if((k.dir == 0 || k.dir == 3) && k.dis > len/2){
		k.dis -= len/2;
		a = go(x, y, len/2, {1, 2, 3}, k), b = go(x, y, len/2, {2, 1, 0}, k);
	}
	else if((k.dir == 0 || k.dir == 1) && k.dis <= len/2)
		a = go(x, y+len/2, len/2, {3, 2, 1}, k), b = go(x, y+len/2, len/2, {2, 3, 0}, k);
        
	else if((k.dir == 1 || k.dir == 2) && k.dis > len/2){
		k.dis -= len/2;
		a = go(x+len/2, y+len/2, len/2, {3, 0, 1}, k), b = go(x+len/2, y+len/2, len/2, {0, 3, 2}, k);
	}
	else if((k.dir == 2 || k.dir == 3) && k.dis <= len/2)
		a = go(x+len/2, y, len/2, {0, 1, 2}, k), b = go(x+len/2, y, len/2, {1, 0, 3}, k);
	
	vis.clear();
	for(io p : a) if(!vis[p]) ret.push_back(p), vis[p] = true;
	for(io p : b) if(!vis[p]) ret.push_back(p), vis[p] = true;
	k.dis = tmp;
	return f[{x, y, len, k}] = ret;
}

int main(){
	cin>>n>>m;
	rep(i,0,m-1) cin>>x[i]>>y[i];
	
	vector<io> ans = solve(0, 0, 1<<n, {0, 1<<n});
	mem(end_x, -1), mem(end_y, -1);
	for(io k : ans){
		int a, b;
		switch(k.dir){
			case 0 : a = 0, b = (1<<n)-k.dis; break;
			case 1 : a = k.dis-1, b = (1<<n)-1; break;
			case 2 : a = (1<<n)-1, b = k.dis-1; break;
			case 3 : a = (1<<n)-k.dis, b = 0; break;
		}
		end_x[k.dir] = a, end_y[k.dir] = b;
	}
	rep(i,0,3){
		if(~end_x[i]) cout<<end_x[i]<<" "<<end_y[i]<<endl;
		else puts("NIE");
	}
	return 0;
}

以上是关于Szkopul (rek). Task Recursive Ant的主要内容,如果未能解决你的问题,请参考以下文章

html jsPatterns.recur.trampoline.js

RECUR宣布与三丽鸥建立NFT战略合作关系,首次将标志性品牌Hello Kitty引入数字藏品空间

10 斐波那契数列

LF.43.In-order Traversal Of Binary Tree(recur+iter)

带有键值集合运算符的 NSPredicate

楼梯有n阶,可以一步上一阶,两阶,问有多少种不同的走法python语言