[解题报告] [NOIP2020]移球游戏

传送🚪

题意

\(n + 1\) 个栈,每个栈最多能放 \(m\) 个球,有 \(n\) 种颜色的球,每种球各有 \(m\) 个。

初始时前 \(n\) 个栈中各放有 \(m\) 个球,第 \(n+1\) 个栈为空。

每次操作可以将一个栈顶的球移到另一个栈顶,构造一个方案,用不多于 \(820000\) 次操作使得颜色相同的所有球都在同一个栈中。

\(n \le 50, m \le 400\)

思路

暴力

比较暴力的思路就是每次把一种颜色的球全部扔到一个栈里,我们记 \(Move(X,Y)\) 为将 \(X\) 栈顶的球移到 \(Y\) 的操作,具体操作如下

  1. 选择一个空栈 \(X\),作为该颜色最终所在的栈。此时其他栈都为满。

  2. 再选择一个栈 \(Y\),把它栈顶的球 \(a\) 移到 \(X\) 中,并将 \(a\) 作为当前我们要处理的颜色。

  3. 对于任意一个栈 \(Z \not = X\)\(Z \not = Y\),如果在 \(Z\) 中有 \(a\) 这类球,就不断进行 \(Move(Z,X)\),直到 \(a\) 到达了 \(Z\) 的栈顶。

  4. \(Move(Z,Y)\), 将 \(a\) 移到 \(Y\) 的空位上。

  5. 不断 \(Move(X,Z)\),直到 \(X\) 中只剩下 \(a\)

  6. \(Move(Y,X)\),将新的 \(a\) 移到 \(X\) 中。

  7. 重复操作。

当所有 \(Z\not = X\)\(Z \not = Y\) 都被处理完后,再选择一个 \(Y'\),然后用类似上面的操作把 \(Y\) 中的 \(a\) 移到 \(X\) 中去。

对于每个球,我们最多需要用 \(2m\) 步将它移动到它应在的栈,共有 \(nm\) 个球,那么总操作次数为 \(O(nm^2)\),在 \(n = 50, m = 85\) 时等于 \(361250\),可以获得 \(40\) 分的好成绩。

(但特别诡异的是我的写法在 \(n = 50, m = 300\) 的数据范围内要跑 \(10^6\) 左右,但是在官方数据中居然通过了这个数据范围。代码在下面)

正解

题目所要求的把同类球放进同个栈里的过程其实可以看做是一个排序的过程,只要对所有栈(包括栈中的元素)排个序,就可以得到我们想要的结果。

先考虑两个栈之间如何排序。设 \(X,Y\) 为当前要排序的两个栈(都为满),\(mid\) 为两个栈内球的中间值(由于有颜色相同的球,所以这里需要一点特殊处理,具体可以看代码),最终我们要将 \(\le mid\) 的球放进 \(X\) 中,\(> mid\) 的球放进 \(Y\) 中。设 \(Z\) 为空栈。具体操作如下。

  1. 算出 \(X\)\(\le mid\) 的球的个数,记为 \(t\)
  2. \(Y\) 中的前 \(t\) 个球移到 \(Z\) 中。(也就是让 \(Y\) 空出 \(t\) 个位置。)
  3. \(X\) 中的球依次弹出,若 \(\le mid\) 就压入 \(Y\) 中,否则压入 \(Z\) 中。
  4. \(Y\) 中的前 \(t\) 个球移到 \(X\) 中,再将 \(Z\) 中的前 \(m - t\) 个球移到 \(X\) 中。那么可以确定此时 \(X\) 栈底的 \(t\) 个球最终要留在 \(X\) 内,\(X\) 栈顶的 \(m - t\) 个求最终要放在 \(Y\) 内。
  5. \(Y\) 中的球 (\(m - t\) 个) 全部移到 \(Z\) 中,将 \(X\) 中的前 \(m - t\) 个球移动到 \(Y\) 中。
  6. \(Z\) 中的球依次弹出,若 \(\le mid\) 就压入 \(X\) 中,否则压入 \(Y\) 中。操作完成。

其中 2 的操作数为 \(t\)\(3,4\) 的操作各为 \(m\)\(5\) 的操作数为 \(2(m-t)\)\(6\) 的操作数为 \(m\),所以总操作数为 \(5m - t\)

我们把上述操作即为 \(Oran(X,Y)\).

接着我们再考虑更多栈之间的排序。

发现我们可以模仿冒泡排序的过程,用 \(Oran(X,Y)\) 代替冒泡排序中的 \(swap(a[x], a[y])\),就可以完成排序,但这样操作次数为 \(O(5n^2m)\),约为 \(5 \times 10^6\),无法接受。

那么可以考虑换一个更高效的排序方式,使用归并排序即可,操作数为 \(O((5m - t + 2m) n \log n) \approx 840000\)

再稍微优化一下。在 \(Oran(X,Y)\) 的过程中可以进行一个特判:若 \(t < \frac{m}{2}\),就把 \(X,Y\) 以及球的大小顺序互换,那么操作数就变为 \(O(4.5m)\),所以总操作数为 \(O(6.5mn\log n) \approx 780000\),可以通过。(但事实上不加这个优化也能过官方数据)

代码

暴力

(期望 40pts,实际(官方数据)70pts)

#include <cassert>
#include <cstdio>
#include <iostream>

#define mkp make_pair
#define fi first
#define se second

using namespace std;

const int _ = 50 + 7;
const int __ = 400 + 7;
const int ___ = 1e7 + 7;
const int lim = 1e7;

int N, m, n, stk[_][__], top[_], num[_][__], X, tot, cur, sum[_], a;
bool flag;
pair<int, int> act[___];
int numAct;

void print() {
  for (int i = 1; i <= N + 1; ++i) {
    cerr << i << ": ";
    for (int j = 1; j <= top[i]; ++j) cerr << stk[i][j] << ' ';
    cerr << endl;
  }
  cerr << endl;
}

void Move(int x, int y) {
  assert(top[x]);
  assert(top[y] != m);
  stk[y][++top[y]] = stk[x][top[x]];
  ++num[y][stk[x][top[x]]], --num[x][stk[x][top[x]]];
  --top[x];
  act[++numAct] = mkp(x, y);
  assert(numAct <= lim);
}

void Pop(int x) {
  for (int i = 1; i <= n; ++i) {
    if (i == x or top[i] == m) continue;
    if (i == X and top[i] == m - 1) continue;
    //if (x != n + 1 and i == X) continue;
    //if (x == X and tot == 1 and top[i] == m - 1) continue;
    Move(x, i); return;
  }
  assert(x != n + 1);
  flag = 1;
  Move(x, n + 1);
}

void Calc() {
  for (int i = 1; i <= n; ++i) sum[i] = 0;
  for (int i = 1; i <= n + 1; ++i)
    for (int j = 1; j <= top[i]; ++j) sum[stk[i][j]] += top[i] - j;
  a = 1;
  for (int i = 1; i <= n; ++i)
    if (sum[i] < sum[a]) a = i;
}

void Pre() {
  X = 0;
  if (!top[n + 1]) { Move(n, n + 1); X = n; a = stk[n + 1][1]; }
  else {
    a = stk[n + 1][1];
    while (num[n + 1][a] != top[n + 1]) Pop(n + 1);
    for (int i = 1; i <= n; ++i)
      if (top[i] < m) { X = i; break; }
  }
}

bool Find() {
  cur = 0; int minx = 0x3f3f3f3f;
  for (int i = 1; i <= n; ++i)
    if (i != X and num[i][a]) { 
      int tmp = 0;
      while (stk[i][top[i] - tmp] != a) ++tmp;
      if (tmp < minx) cur = i, minx = tmp;
    }
  return cur != 0;
}

void Fir() {
  while (Find()) {
    while (stk[cur][top[cur]] != a) Pop(cur);
    if (num[n + 1][a] != top[n + 1]) {
      Move(cur, X);
      while (num[n + 1][a] != top[n + 1]) Pop(n + 1);
      Move(X, n + 1);
    }
    else Move(cur, n + 1);
  }
}

void Sec() {
  int t = 0;
  for (int i = 1; i <= n; ++i)
    if (i != X and (!t or top[t] > top[i])) t = i;
  assert(t);
  while (top[t] < m - 1 and num[X][a]) {
    if (stk[X][top[X]] == a) Move(X, n + 1);
    else Move(X, t);
  }
  int tmp = X; X = t;
  while (num[tmp][a]) {
    while (stk[tmp][top[tmp]] != a) Pop(tmp);
    if (num[n + 1][a] != top[n + 1]) {
      Move(tmp, X);
      while (num[n + 1][a] != top[n + 1]) Pop(n + 1);
      Move(X, n + 1);
    }
    else Move(tmp, n + 1);
  }
}

int main() {
  cin >> N >> m; n = N;
  for (int i = 1; i <= N; ++i) {
    top[i] = m;
    for (int j = 1; j <= m; ++j) { scanf("%d", &stk[i][j]); ++num[i][stk[i][j]]; }
  }
  for (n = N; n >= 2; --n) {
    Pre();
    Fir();
    Sec();
  }

  if (top[1] < top[2]) { while(top[1]) Move(1, 2); }
  else { while (top[2]) Move(2, 1); }

  //cerr << "num: " << numAct << endl;
  printf("%d\n", numAct);
  for (int i = 1; i <= numAct; ++i) printf("%d %d\n", act[i].fi, act[i].se);
  return 0;
}

正解

#include <cassert>
#include <cstdio>
#include <iostream>

#define mkp make_pair
#define fi first
#define se second

using namespace std;

const int _ = 50 + 7;
const int __ = 400 + 7;
const int ___ = 1e6 + 7;

int n, m, p[_][__], top[_], numAct, num[_], sta[_], rec, lim;
bool flag = 0;
pair<int, int> act[___];

void Mark(int x, int y) {
  for (int i = 1; i <= n; ++i) num[i] = sta[i] = 0;
  for (int i = 1; i <= m; ++i) ++num[p[x][i]], ++num[p[y][i]];
  int cnt = 0;
  for (int i = 1; i <= n; ++i) {
    if (cnt + num[i] <= m) cnt += num[i], sta[i] = -1;
    else {
      sta[i] = 0, lim = m - cnt;
      for (int j = i + 1; j <= n; ++j) sta[j] = 1;
      break;
    }
  }
}

bool Check(int x) {
  if (sta[x] != 0) return sta[x] == -1;
  if (rec < lim) { ++rec; return 1; };
  return 0;
}

void Move(int x, int y) {
  act[++numAct] = mkp(x, y);
  p[y][++top[y]] = p[x][top[x]]; --top[x];
}

void Oran(int x, int y, int z = n + 1) {
  Mark(x, y);
  int cnt = 0; rec = 0;
  for (int i = 1; i <= m; ++i)
    if (Check(p[x][i])) ++cnt;
  for (int i = 1; i <= cnt; ++i) Move(y, z);
  rec = 0;
  for (int i = m; i >= 1; --i) Move(x, Check(p[x][i]) ? y : z);
  for (int i = 1; i <= cnt; ++i) Move(y, x);
  for (int i = 1; i <= m - cnt; ++i) Move(z, x);
  for (int i = 1; i <= m - cnt; ++i) Move(y, z);
  for (int i = 1; i <= m - cnt; ++i) Move(x, y);
  for (int i = m; i >= 1; --i) Move(z, Check(p[z][i]) ? x : y);
}

void Pour(int x, int y) {
  int tmp = top[x];
  for (int i = 1; i <= tmp; ++i) Move(x, y);
}

int id[_], pos[_];

void Arr(int l, int r, int z = n + 1) {
  for (int i = l; i <= r; ++i)
    if (pos[i] != i) {
      if (id[i]) {
        Pour(i, z);
        id[z] = id[i], pos[id[i]] = z, id[i] = 0;
      }
      Pour(pos[i], i);
      id[pos[i]] = 0, z = pos[i], pos[i] = i, id[i] = i;
    }
}

int maxn[_];

#define mid ((l + r) >> 1)
void Solve(int l, int r) {
  if (l == r) return;
  Solve(l, mid); Solve(mid + 1, r);
  int t1 = l, t2 = mid + 1;
  for (int i = l; i <= r; ++i) {
    maxn[i] = 0;
    for (int j = 1; j <= m; ++j) maxn[i] = max(maxn[i], p[i][j]);
  }
  int idx = l - 1;
  while (t1 <= mid and t2 <= r) {
    while (t1 <= mid and maxn[t1] <= maxn[t2]) { Oran(t1, t2); id[t1] = ++idx, pos[idx] = t1; ++t1; }
    if (t1 > mid) break;
    while (t2 <= r and maxn[t2] <= maxn[t1]) { Oran(t2, t1); id[t2] = ++idx, pos[idx] = t2; ++t2; }
  }
  while (t1 <= mid) { id[t1] = ++idx, pos[idx] = t1; ++t1; }
  while (t2 <= r) { id[t2] = ++idx, pos[idx] = t2; ++t2; }
  Arr(l, r);
}
#undef mid

int main() {
  cin >> n >> m;
  for (int i = 1; i <= n; ++i) {
    top[i] = m;
    for (int j = 1; j <= m; ++j) { scanf("%d", &p[i][j]); maxn[i] = max(maxn[i], p[i][j]); }
  }
  Solve(1, n);
  cerr << "num: " << numAct << endl;
  cout << numAct << endl;
  for (int i = 1; i <= numAct; ++i) printf("%d %d\n", act[i].fi, act[i].se);
  return 0;
}
posted @ 2020-12-26 11:34  BruceW  阅读(185)  评论(0编辑  收藏  举报