【NOIP2020】移球遊戲 - 分治

題目描述

小 C 正在玩一個移球遊戲,他面前有 \(n+1\) 根柱子,柱子從 \(1 \sim n + 1\) 編號,其中 \(1\) 號柱子、\(2\) 號柱子、……、\(n\) 號柱子上各有 \(m\) 個球,它們自底向上放置在柱子上,\(n + 1\) 號柱子上初始時沒有球。這 \(n \times m\) 個球共有 \(n\) 種顏色,每種顏色的球各 \(m\) 個。

初始時一根柱子上的球可能是五顏六色的,而小 C 的任務是將所有同種顏色的球移到同一根柱子上,這是唯一的目標,而每種顏色的球最後放置在哪根柱子則沒有限制。

小 C 可以通過若干次操作完成這個目標,一次操作能將一個球從一根柱子移到另一根柱子上。更具體地,將 \(x\) 號柱子上的球移動到 \(y\) 號柱子上的要求爲:

  1. \(x\) 號柱子上至少有一個球;
  2. \(y\) 號柱子上至多有 \(m - 1\) 個球;
  3. 只能將 \(x\) 號柱子最上方的球移到 \(y\) 號柱子的最上方。

小 C 的目標並不難完成,因此他決定給自己加加難度:在完成目標的基礎上,使用的操作次數不能超過 \(820000\)。換句話說,小 C 需要使用至多 \(820000\) 次操作完成目標。

小 C 被難住了,但他相信難不倒你,請你給出一個操作方案完成小 C 的目標。

思路

首先若只有兩個顏色的球,可以用如下 gif 演示的方案用空柱子分離

具體過如下:

\(a\) 個白球與 \(b\) 個黑球

1.將第二個柱騰出 \(a\) 個空位

2.分離第一個柱子的黑白球

3.放回第一個柱子

4.騰出第二個柱子

5.把第一個柱子的黑球放到第二個柱子

6.分離第三個柱子

此方案的移動次數爲 \(5m-a\)

所以得到了多個顏色的做法:分治求解區間 \([l,r]\) 的柱子,設一個閥值 \(mid=\frac{l+r}{2}\),將小於等於 \(x\) 的數和大於 \(x\) 的數視作兩個顏色,枚舉 \([l,mid]\)\([mid+1,r]\) 兩個柱子,按照上述方法分離兩種顏色的球,再分治求解兩個子區間.

注意到枚舉的兩根柱子兩種顏色數量不一定相等,於是就只還原多的那種顏色的柱子.

操作次數看成 \(5m\),總的操作次數則是 \(f(n)=2f(\frac{n}{2})+5mn=5mn\log n\le 820000\)

#include <utility>
#include <cstring>
#include <cstdio>
using namespace std;
const int maxn = 400 + 10;
int n,m,tot,pos[maxn][maxn];
pair<int,int> ans[820001];
bool fin[maxn];
inline void move(int x,int y) {
	ans[++tot] = make_pair(x,y);
	pos[y][++pos[y][0]] = pos[x][pos[x][0]--];
}
inline void solve(int l,int r) {
	if (l == r) return;
	int mid = l+r>>1;
	memset(fin,false,sizeof fin);
	for (int i = l;i <= mid;i++)
		for (int j = mid+1;j <= r;j++) if (!fin[i] && !fin[j]) {
			int s = 0,cnt = 0;
			for (int k = 1;k <= m;k++) s += (pos[i][k] <= mid)+(pos[j][k] <= mid);
			int a = (s >= m ? i : j),b = (s >= m ? j : i);
			for (int k = 1;k <= m;k++)
				if (s >= m) cnt += pos[a][k] <= mid;
				else cnt += pos[a][k] > mid;
			for (int k = 1;k <= cnt;k++) move(b,n+1);
			for (int k = m;k >= 1;k--)
				if (s >= m) move(a,pos[a][k] <= mid ? b : n+1);
				else move(a,pos[a][k] > mid ? b : n+1);
			for (int k = 1;k <= cnt;k++) move(b,a);
			for (int k = 1;k <= m-cnt;k++) move(n+1,a);
			for (int k = 1;k <= m-cnt;k++) move(b,n+1);
			for (int k = 1;k <= m-cnt;k++) move(a,b);
			for (int k = m;k >= 1;k--)
				if (s >= m) move(n+1,pos[a][0] < m && pos[n+1][k] <= mid ? a : b);
				else move(n+1,pos[a][0] < m && pos[n+1][k] > mid ? a : b);
			fin[a] = true;
		}
	solve(l,mid);
	solve(mid+1,r);
}
int main() {
	scanf("%d%d",&n,&m);
	for (int i = 1;i <= n;i++) {
		pos[i][0] = m;
		for (int j = 1;j <= m;j++) scanf("%d",&pos[i][j]);
	}
	solve(1,n);
	printf("%d\n",tot);
	for (int i = 1;i <= tot;i++) printf("%d %d\n",ans[i].first,ans[i].second);
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章