解题报告 smoj 2019初二创新班(2019.6.15)
解题报告 smoj 2019初二创新班(2019.6.15)
时间:2019.6.18
T1:合唱队形二
题目描述
将 \(n\) 个身高互不相同的学生排成合唱队形,且要求任意相邻两个人的身高差不超过 \(k\),有多少种方案?
定义合唱队形为 \(\exist mid, a[1 .. mid] \text {单调上升}, a[mid +1.. n] \text {单调下降}\)。且要求整个序列不能仅仅只是单调上升或仅仅单调下降。
答案对 \(1234567891\) 取模。
分析
让我们设想一下,如果我们就是音♂乐老师,我们会怎样排好位置?(符号唐突)
大概的想法就是:把同学们从高到矮排好,然后依次向队列两端放人嘛!
为了排出合法(身高差不超过 \(k\))的合唱队列,我们还需要知道当前队列两端各自的身高。
先将所有同学的身高按照从大到小的顺序排列。不妨设 \(f(i, l, r)\) 表示将前 \(i\) 位同学排成合唱队列,且满足合唱队列的两端分别是同学 \(l\) 和同学 \(r\) 的方案数。
转移时,将 \(n\) 位同学依次加入到队列中就行了。使用“我为人人”的方式,令 \(j\) 为 \(i\) 的下一位同学(即 \(j = i + 1\)),要么将 \(j\) 加入到左端,要么加入到右端。分别检查一下 \(j\) 和 \(l\) 、\(r\) 的身高差是否不超过 \(k\) ,若是则可以加入到对应的那一端,方案数加上 \(f(i, l, r)\)。
if (a[l] - a[j] <= k) f[j][j][r] += f[i][l][r]; // 将 j 加入到左端,l 变成 j
if (a[r] - a[j] <= k) f[j][l][j] += f[i][l][r]; // 将 j 加入到右端,r 变成 j
当然,如果两个都不满足,那么 f[j][j][r]
(f[j][l][j]
)什么都不加上。注意 a[l]
是一定大于 a[j]
的,因为我们已经提前将数组排过序了。
总时间复杂度为 \(O(n ^ 3)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 50 + 10;
const int kMod = 1234567891;
typedef long long LL;
int T;
int n, k, a[kMaxN];
LL f[kMaxN][kMaxN][kMaxN];
inline bool Comp(int x, int y) { return x > y; }
int main() {
freopen("2905.in", "r", stdin);
freopen("2905.out", "w", stdout);
scanf("%d", &T);
while (T--) {
scanf("%d %d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
sort(a + 1, a + n + 1, Comp);
memset(f, 0, sizeof(f));
f[1][1][1] = 1;
for (int i = 1; i <= n - 1; i++)
for (int l = 1; l <= i; l++)
for (int r = 1; r <= i; r++) {
int j = i + 1;
if (a[l] - a[j] <= k)
(f[j][j][r] += f[i][l][r]) %= kMod;
if (a[r] - a[j] <= k)
(f[j][l][j] += f[i][l][r]) %= kMod;
}
LL ans = 0;
// l, r 要从 2 开始,因为合唱队形不能单调上升 / 单调下降,
// 也就是说两端的同学都不能是最高的一个,即 1 号同学。
for (int l = 2; l <= n; l++)
for (int r = 2; r <= n; r++)
(ans += f[n][l][r]) %= kMod;
printf("%lld\n", ans);
}
return 0;
}
优化
有一个简单却有效的优化。观察状态转移方程(转移代码):
if (a[l] - a[j] <= k) f[j][j][r] += f[i][l][r]; // 将 j 加入到左端,l 变成 j
if (a[r] - a[j] <= k) f[j][l][j] += f[i][l][r]; // 将 j 加入到右端,r 变成 j
我们发现最左端或者最右端始终都是 \(j\),也就意味着一直都会有 \(l = i\) 或 \(r = i\)(否则 f
数组为 0)。这很好理解。我们将 \(j\) 加入到队列的某一端,那么肯定要么在最左端,要么在最右端。
状态中有一个维度是多余的。不妨重新设计状态和方程。
设 \(f[i][j]\) 表示对于前 \(i\) 位同学排好的合唱队形,其最左端为 \(i\),最右端为 \(j\) 的方案数。
可以发现 \(i\) 在最左端和最右端的情况是对称的。也就是说这两种情况(左右端分别为 \(i, j\) 的某条队列 \([i, \dots, j]\) 与左右端分别为 \(j, i\) 的某条队列 \([j, \dots, i]\))方案数是相同的。因此我们保证 \(j < i\)。
此题数据其实可以开到 \(n = 1000\)。容易写出转移方程。详细见代码。
代码(优化后)
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 50 + 10;
const int kMod = 1234567891;
typedef long long LL;
int T;
int n, k, a[kMaxN];
inline bool Comp(int x, int y) { return x > y; }
// f[i][j] 表示对于前 i 位同学排好的合唱队形,其中左端为 i,右端为 j 的方案数
// 我为人人
LL f[kMaxN][kMaxN];
int main() {
freopen("2905.in", "r", stdin);
freopen("2905.out", "w", stdout);
scanf("%d", &T);
while (T--) {
scanf("%d %d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
sort(a + 1, a + n + 1, Comp);
memset(f, 0, sizeof(f));
f[2][1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i - 1; j++) {
if (a[i] - a[i + 1] <= k)
f[i + 1][j] = (f[i + 1][j] + f[i][j]) % kMod;
if (a[j] - a[i + 1] <= k)
f[i + 1][i] = (f[i + 1][i] + f[i][j]) % kMod;
}
}
LL ans = 0;
for (int i = 2; i <= n - 1; i++)
ans = (ans + f[n][i]) % kMod;
printf("%lld\n", ans * 2 % kMod);
}
return 0;
}
T2:旅游(重题)
原题链接及题解
原题链接见 smoj 1797 旅游,题解见洛谷博客 2018 - 10 - 23 石门中学 2018初二创新班(6) 解题报告。