「JOI Open 2016」JOIRIS
提交地址
statement
译自 JOI Open 2016 T1「JOIRIS」
「井」、「骨牌」及「方块」的翻译参考了俄罗斯方块(Tetris)。
JOIRIS 的游戏区域名叫「井」,是一个宽度为 \(N\),高度足够大的矩形网格。位于左数第 \(i\) 列,从下往上数第 \(j\) 列的格子记作 \((i,j)\)。游戏过程中,每个格子要不有一个方块,要不没有方块。
开始时,在第 \(i\) 列有且仅有 \((i,1), (i,2),\dots, (i, A_i)\) 有方块。
接下来,\(10^4\) 个 \(1×K\) 的骨牌一个个下落,玩家要依次放置骨牌。每次放置骨牌按照如下方式进行:
- 玩家先选择骨牌是横向放置还是纵向放置。
- 若选择纵向,玩家还需再选择一个整数 \(x\) \((1 \le x \le N)\)。一个骨牌会下落到第 \(x\) 列最上方方块的上面一行。若第 \(x\) 列没有方块,骨牌会下落到井底。
- 若选择横向,玩家还需再选择一个整数 \(x\) \((1 \le x \le N-K+1)\)。一个骨牌会下落到第 \(x\) 列至第 \(x+K-1\) 列最上方方块的上面一行。若第 \(x\) 列至第 \(x+K-1\) 列没有方块,骨牌会下落到井底。
- 每个骨牌停止下落后,系统将从井底往上逐行检查,如果有一行格子被方块填满,该行的所有方块都会消失,且上方的所有方块向下移动 \(1\) 格。
- 此时检查井中是否有方块,如果井中没有方块,游戏结束,玩家胜利,否则玩家开始放置下一个骨牌。
保证开始时最底下一行没有被方块填满。请判断玩家能否胜利,如果可能,则输出一种方案。
constraints
对于所有数据,\(2\le N\le 50,\) \(1\le K\le N,\) \(0\le A_i \le 50\)。
子任务编号 | 分值 | \(K\) | \(N\) |
---|---|---|---|
\(1\) | \(15\) | \(K=2\) | \(N\) 是偶数 |
\(2\) | \(15\) | \(K=2\) | \(N\) 是奇数 |
\(3\) | \(15\) | \(K\) 整除 \(N\) | - |
\(4\) | \(55\) | - | - |
sol
纯构造
首先认为所有数组下标是从 \(0\) 开始的。
定义 \(b_i = \left( \sum\limits_{j = 0} ^ {n - 1} [j \equiv i \pmod{k}] a_j \right) \bmod k\)。
我们断言:
解的判定法则
记 \(p = (n - 1) \bmod k\)。
玩家能赢即能删空所有方块,当且仅当,\(b_0 = b_1 = \cdots = b_p\) 和 \(b_{p + 1} = b_{p + 2} = \cdots = b_{k - 1}\) 同时成立。
下面分别证明充分性和必要性。
充分性
即证明:若能删空所有方块,则 \(b_0 = b_1 = \cdots = b_p\) 和 \(b_{p + 1} = b_{p + 2} = \cdots = b_{k - 1}\) 同时成立。
考虑将操作反过来进行。
初始时 \(b\) 全为 \(0\),结论成立。然后分析进行一次操作:
- 拿走第 \(i\) 列的竖的骨牌不会改变 \(a_i \bmod k\) 的结果,自然也不会影响数组 \(b\) 的结果,结论成立。
- 拿走第 \(i\) 行的横的骨牌会同时给 \(a_i, a_{i + 1}, \cdots, a_{i + k - 1}\) 减一,因为这是连续的 \(k\) 个数,所以数组 \(b\) 的每一位都会减一,结论成立。
- 全局加一,此时将数组 \(a\) 按 \(k\) 为块长分块,前面大小为 \(k\) 的块同 2. 一样分析即可,最后一个 \(n\) 所在的块只会让 \(b_0, b_1, \cdots , b_p\) 加一,结论成立。
直到变成初始状态停止操作,此时充分性得证。
必要性
即证明:若 \(b_0 = b_1 = \cdots = b_p\) 和 \(b_{p + 1} = b_{p + 2} = \cdots = b_{k - 1}\) 同时成立,则能删空所有方块。
给出如下的构造来证明。
step1
对于 \(i = 1, \cdots, n - 1\),如果 \(a_{i - 1} > a_i\),就在第 \(i\) 列放上竖的骨牌,直到 \(a_{i - 1} \le a_i\)。
然后我们得到:\(a_0 \le a_1 \le \cdots \le a_{n - 1}\)。
step2
对于 \(i = 0, \cdots, a_{n - 1}\),在第 \(i\) 行以右对齐的方式尽可能多的放置横的骨牌。
然后我们有:\(a_{k - 1} = a_k = \cdots = a_{n - 1}\)。
因为如果不满足这个限制,就一定可以继续放置横的骨牌。
step3
对于 \(i = 0, \cdots, k - 2\),在第 \(i\) 列放置足够多的竖的骨牌,具体数量不小于每行左侧空出来的位置数量。
然后我们有:\(a_{k - 1} = a_k = \cdots = a_{n - 1} = 0\)。
因为对于每行左侧空的都填满了,右侧本就填满了,所以会导致所有行都被消除,但是加入方块数量可能大于删除方块数量,导致剩余部分方块。
step4
因为开始有等式 \(b_0 = b_1 = \cdots = b_p\) 和 \(b_{p + 1} = b_{p + 2} = \cdots = b_{k - 1}\) 成立,而类似分析充分性可以知道每次操作后等式依然成立。
那么在「step3」后有:\(a_0 \equiv a_1 \equiv \cdots \equiv a_p \pmod k\) 和 \(a_{p + 1} \equiv a_{p + 2} \equiv \cdots \equiv a_{k - 1} \equiv 0 \pmod k\)。
此时容易通过放置竖的骨牌消除第 \(p + 1\) 列至第 \(k - 2\) 列。
step5
在「step4」后有:\(a_0 \equiv a_1 \equiv \cdots \equiv a_p \pmod k\) 和 \(a_{p + 1} = a_{p + 2} = \cdots = a_{n - 1} = 0\)。
容易放置竖的骨牌使 \(a_0 = a_1 = \cdots = a_p\)。
又因为 \(p \equiv n-1 \pmod k\),所以第 \(p + 1\) 列到第 \(n - 1\) 列可以恰好用横的骨牌填充。
最后容易消除所有方块。
必要性得证。故「解的判定法则」正确。
实现
根据构造方法可以得到一种写法,但事实上「step4」和「step5」中放置竖的步骤是不必要的。
首先按照如下方法执行「step3」。
初始化数组 \(c\) 全为 \(0\),而 \(a\) 为「step2」后得到的数组。
对于 \(i = 0, \cdots, n - 2\),如果 \(i \not\equiv k - 1\pmod k\),则对于 \(c_0, c_1, \cdots c_{i \bmod k}\) 加上 \(a_{i + 1} - a_i\)。
然后对于 \(i = 0, \cdots , k - 2\),如果 \(c_i > 0\) 则在第 \(i\) 列加入竖的骨牌,同时 \(c_i \leftarrow c_i - k\),直到 \(c_i \le 0\)。
最后令 \(c_i \leftarrow -c_i\),此时 \(c_i\) 即第 \(i\) 列的真实的方块数。
因为 \(0 \le c_i < k\),而由「step4」知 \(c_0 \equiv c_1 \equiv \cdots \equiv c_p \pmod k\) 和 \(c_{p + 1} \equiv c_{p + 2} \equiv \cdots \equiv c_{k - 1} \equiv 0 \pmod k\)。
这等价于 \(c_0 = c_1 = \cdots = c_p\) 和 \(c_{p+1} = c_{p + 2} = \cdots = c_{k - 1} = c_{k} = \cdots = c_{n - 1} = 0\)。
所以直接执行「step5」中放置水平骨牌的步骤即可。
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 55;
int n, k, a[N], b[N], c[N];
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) {
cin >> a[i];
b[i % k] = (b[i % k] + a[i]) % k;
}
int p = (n - 1) % k;
for (int i = 0; i <= p; i++) {
if (b[i] != b[p]) {
cout << "-1\n";
return 0;
}
}
for (int i = p + 1; i <= k - 1; i++) {
if (b[i] != b[k - 1]) {
cout << "-1\n";
return 0;
}
}
vector<pair<int, int>> ans;
for (int i = 1; i < n; i++) {
while (a[i - 1] > a[i]) {
a[i] += k;
ans.emplace_back(1, i);
}
}
for (int i = 0; i <= n - 2; i++) {
for (int j = a[i]; j < a[i + 1]; j++) {
for (int x = i - k + 1; x >= 0; x -= k) {
ans.emplace_back(2, x);
}
if (i % k != k - 1) {
c[i % k]++;
}
}
}
for (int i = k - 2; i >= 0; i--) {
c[i] += c[i + 1];
}
for (int i = 0; i <= k - 2; i++) {
while (c[i] > 0) {
c[i] -= k;
ans.emplace_back(1, i);
}
}
for (int i = p + 1; i < n; i += k) {
for (int j = 1; j <= -c[0]; j++) {
ans.emplace_back(2, i);
}
}
cout << ans.size() << "\n";
for (auto [i, j] : ans) {
cout << i << " " << j + 1 << "\n";
}
return 0;
}
构造+线性同余方程组
认为数组的下标从 \(1\) 开始。
这个做法的出发点是首先注意到放置横的骨牌可能使一列中产生不连续的方块,这是我们需要避免的。
而我们想在放置横的骨牌后立即通过行消除的方式把横的骨牌删去。
norm 操作
记 \(\max\) 为 \(a\) 中的最大值,然后令 \(\max \leftarrow k \lfloor \dfrac{\max}{k}\rfloor\)。
对于 \(i = 1, \cdots , n\),如果 \(a_i < \max\),则在 \(i\) 列放置竖的骨牌,直到 \(a_i \ge \max\)。
游戏会执行消除操作,即将每个数减去 \(a\) 中的最小值。
然后我们可以将所有数变成 \(\bmod k\) 的结果。
记作 norm 操作。
构造
首先执行 norm 操作,所有数属于 \([0, k)\)。
在 \(i\) 列放置 \(w\) 个横的骨牌后,会落在区间 \([i, i + k - 1]\) 中最高的方块上,令 \(\max\) 为方块高度。
对于 \(j \in [1, i) \cup [i + k, n]\),如果 \(a_j < \max + w\),则在第 \(j\) 列放置竖的骨牌,直到 \(a_j \ge \max + w\)。
此时触发行消除,\(w\) 个横的骨牌被删除,容易维护数组 \(a\)。
最后再执行 norm 操作。
这样我们可以任意执行放置横的骨牌,而保证每一列中方块都是一段前缀。
线性同余方程组
最后我们希望通过构造的操作删掉所有方块。
记 \(b_i\) 为在位置 \(i\) 放置横的骨牌的次数,其中 \(1 \le i \le n - k + 1\)。
因为每次放置横的骨牌就会执行构造操作,所以知道放置顺序与结果无关。
同时 \(b_i \in [0, k)\),因为构造操作实质上每次会执行补集消除然后执行 norm 操作,恰好 \(k\) 次操作会使数组回到最初状态。
记 \(t_i\) 为每列放置竖的骨牌数量,\(d\) 为行消除的次数。
对于 \(i = 1, \cdots, n\),我们有 \(a_i + kt_i - d + \sum\limits_{j = max(1, i - k +1)} ^ i b_j = 0\)。
\(\Rightarrow a_i + \sum\limits_{j = max(1, i - k +1)} ^ i b_j \equiv d \pmod k\)。
这是一个线性同余方程组。
枚举 \(d \in [0, k)\)。
观察 \(i = 1\),有 \(a_1 + b_1 \equiv d \pmod k \Leftrightarrow b_1 = (d - a_1) \bmod k\)。
然后对于 \(j = 1, \cdots, k\),\(a_j \leftarrow (a_j + b_i) \bmod k\)。
依次考虑 \(i = 1, \cdots, n\),每次执行同样的操作,即可求出一组 \(b\)。
最后检验这组解是否合法即可。
如果对于所有 \(d\) 的解都不合法,说明原问题无解。
然后拿这组解跑构造操作即可消除所有方块。
实现
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 55;
int n, k, a[N], b[N];
vector<pair<int, int>> ans;
void norm() {
int mx = 0;
for (int i = 1; i <= n; i++) {
mx = max(mx, a[i]);
}
mx = mx / k * k;
for (int i = 1; i <= n; i++) {
while (a[i] < mx) {
a[i] += k;
ans.emplace_back(1, i);
}
}
int mn = 1e9;
for (int i = 1; i <= n; i++) {
mn = min(mn, a[i]);
}
for (int i = 1; i <= n; i++) {
a[i] -= mn;
}
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
bool flag1 = 0;
for (int d = 0; d < k; d++) {
static int c[N];
for (int i = 1; i <= n; i++) {
c[i] = a[i] % k;
}
for (int i = 1; i <= n - k + 1; i++) {
b[i] = (d + k - c[i]) % k;
for (int j = i; j <= i + k - 1; j++) {
c[j] = (c[j] + b[i]) % k;
}
}
bool flag2 = 1;
for (int i = 1; i <= n; i++) {
if (c[i] != d) {
flag2 = 0;
break;
}
}
if (flag2) {
flag1 = 1;
break;
}
}
if (flag1 == 0) {
cout << "-1\n";
return 0;
}
norm();
for (int i = 1; i <= n - k + 1; i++) {
for (int j = 1; j <= b[i]; j++) {
ans.emplace_back(2, i);
}
int mx = 0;
for (int j = i; j <= i + k - 1; j++) {
mx = max(mx, a[j]);
}
mx += b[i];
for (int j = 1; j <= n; j++) {
if (j < i || i + k <= j) {
while (a[j] < mx) {
a[j] += k;
ans.emplace_back(1, j);
}
a[j] -= b[i];
}
}
norm();
}
cout << ans.size() << "\n";
for (auto [i, j] : ans) {
cout << i << " " << j << "\n";
}
return 0;
}
小结
方法 1 比较难想到,但是方法 2 就少了很多需要构造的东西,相对来说思路比较简单。
理论上两种做法的复杂度均可做到 \(O(n ^ 2)\),这里认为 \(O(n) = O(V)\)。
但是在这里的实现中,方法 1 是 \(O(n^2)\),而方法 2 是 \(O(n ^ 3)\),但是操作数都是 \(O(n ^ 2)\) 的。
方法 2 想优化可以利用同余系的关系列出关于 \(d\) 的同余方程组,然后使用 crt 解出一个 \(d\),然后就是 \(O(n ^ 2)\) 的了。具体的就没有实现该做法了。