[dp 记录] agc020F arcs on a circle

发布时间 2023-03-22 21:16:51作者: purplevine

神题。yhx 的讲解 非常好、非常自然。

题意:

给定 \(c\)\(n\) 段长度为 \(a_i\) 的弧,每条弧的起点在圆周上均匀随机一个位置,求所有弧的并集覆盖圆周的概率。

\(c \leq 50, n \leq 6\)

环上的问题并不好处理,因此寻求链是自然的。钦定一段弧的起点是一段弧的起点看着不错,这样子被覆盖的始终是一段前缀,然后呢?

现在需要用线段覆盖区间。为了提防一段弧同时覆盖前缀与后缀,把起点定位最长弧的左端点,每段弧就只会覆盖一个区间了。

弧是离散的情况是好做的。注意到,我们之所以需要离散这个条件,是因为我们需要比较两段弧的右端点的位置。

结论:两段弧是否相交仅与整数部分的数值与小数部分的大小关系有关。

因此全排列枚举大小关系即可。因为均匀分布,每种大小关系出现的可能性相等。由于连续分布,不用在意两数相等。

不妨让所有弧的坐标均是 \(\dfrac{1}{n}\) 的整数倍,则这个问题成为连续问题。

增量研究是好的。钦定一个考虑的顺序有助于递推。对于此题,从小到大考虑左端点可以保证只用记录最长能延申到的右端点。

每段弧的左端点只有 \(c\) 个可选位置,因此每个位置都有放或不放唯一一段对应弧的选择。有可能这段弧已经出现,多状压一维记录已经出现的弧的集合。

\(f_{i, j, k}\) 表示:只考虑左端点小于等于 \(i\) 的弧、用的弧的集合为 \(j\)(下标即为人为赋予的相对顺序)、右端点连续延伸到 \(k\) 的方案数。转移平凡。

第一维被压掉了,因为同一层只会是小的 \(k\) 往大的 \(k\) 转移。两层间不用清空,对应左端点在此处的弧不被放入的情况。

#include <bits/stdc++.h>
using namespace std;
const int M = 55;
double f[33][305];
// f_{i, j, k} : 只考虑左端点 <= i 的线段,长度集合为 j,右端点延申至 k 的方案数
// i 一维被滚掉了,从小到大转移保证同层状态不会转移,因为有不放线段的转移,不用清空
int a[M], n, c;
int solve() {
  memset(f, 0, sizeof(f)); f[0][a[n] * (n + 1)] = 1;
  for (int i = 0; i < (n + 1) * c; i++) if (i % (n + 1)) {
    int t = i % (n + 1) - 1, r = min((n + 1) * c, i + a[t] * (n + 1));
    for (int j = 0; j < (1 << n); j++) if (!(j & (1 << t))) {
      for (int k = i; k <= (n + 1) * c; k++) 
        f[j | (1 << t)][max(r, k)] += f[j][k];
    }
  }
  return f[(1 << n) - 1][(n + 1) * c];
}
int main() {
  scanf("%d %d", &n, &c); --n;
  for (int i = 0; i <= n; i++) scanf("%d", &a[i]);
  sort(a, a + n + 1);
  int cnt = 0; double ans = 0;
  do {
    ans += solve(), ++cnt;
  } while (next_permutation(a, a + n));
  ans /= cnt;
  for (int i = 1; i <= n; i++) ans /= c;
  printf("%.11lf\n", ans);
}