解题报告 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) 解题报告

posted @ 2019-06-21 15:41  longlongzhu123  阅读(49)  评论(0编辑  收藏  举报