2018 ITMO Beijing Camp Day 2 ITMO U contest

D. DotA Quals

题意:有2^n (n <= 10)位选手,实力有一个绝对的排名,你处于第k (k <= 2^n) 位。每一轮剩下的人两两随机配对作战,一场对战中总是排名考前的人获胜,并且输家会退出比赛。让你求出你的期望轮数。

观察:一共只会进行n轮比赛,P(X)表示X时间发生的概率,答案是P(打到第一轮)+ P(打到第二轮)+ ... + P(打到第n轮)。那么P(打到第i轮),计作P(i),该如何计算呢?画个图就比较明显了,为了参加第i轮,那么我们一定要在第i-1轮出线,脑补一下对战图(是一个二叉树),也就是让我们成为我们所在分赛站(在图中是一颗子树)所有2^(i-1) 队伍中最强的队伍,所以P(i) = cb((2^n)-k, (2^(i-1))-1)/cb((2^n)-1, (2^(i-1))-1)。这里cb(n, k) 指组合数,即从n个物品中选k个的方法数。公式中,分子代表从比我们弱的(2^n-k)个人中取(2^(i-1)-1)个人的方法数,-1是因为要除去我们自身,分母就是从所有除了自身的人中选出那些人的方法数。

方法:因为要求精度,所以可以用log() 和 exp()。

code:

 1 /*
 2  by skydog
 3  */
 4 #include <iostream>
 5 #include <cstdio>
 6 #include <vector>
 7 #include <utility>
 8 #include <algorithm>
 9 #include <cmath>
10 #include <cstring>
11 #include <map>
12 #include <set>
13 #include <stack>
14 #include <queue>
15 #include <deque>
16 #include <cassert>
17 #include <list>
18 using namespace std;
19 typedef long long ll;
20 typedef pair<int, int> ii;
21 typedef pair<ll, ll> l4;
22 
23 #define mp make_pair
24 #define pb push_back
25 
26 const int N = 11;
27 const int maxn = 1<<N;
28 double f[maxn];
29 double cb(int n, int k)
30 {
31     if (n < k || k < 0)
32         return 0;
33     return exp(f[n]-f[k]-f[n-k]);
34 }
35 int main()
36 {
37     f[1] = 0;
38     for (int i = 2; i < maxn; ++i)
39         f[i] = f[i-1] + log(i);
40     int n, k;
41     scanf("%d %d", &n, &k);
42     double ans = 0;
43     for (int i = 1; i <= n; ++i)
44         ans += cb((1<<n)-k, (1<<(i-1))-1)/cb((1<<n)-1, (1<<(i-1))-1);
45     printf("%.9lf
", ans);
46 }
View Code

回顾:下次可以画个图,整体考虑一下。现场学弟想了一个dp[i][j],表示第i轮,有j个比自己强的人的概率。也可以推,但是比起正解麻烦了一点。

G. Game of Chairs

题意:c种颜色,有n把椅子排成一列,没把椅子有一个颜色a[i], (n, c <= 1e6,  1<= a[i] <= c)。保证每种颜色的椅子都有。下面让你选一把椅子坐下,裁判会随机说一个1-c的颜色,你要从你所在的椅子到一把对应颜色的椅子,经过的距离是椅子编号之差。下面请你选择最优的初始位置,使得期望移动距离最小,输出这个最小距离。

观察

  每种颜色都是等概率取的,所以计算某一个位置i的期望只需要考虑到当前位置各个颜色的最短距离和D(i)就好了,不妨设D(i, k)为颜色为k的椅子与第i把椅子的最小距离,即D(i, k) = min{abs(i-j), where a[j] == k},那么D(i) = sum of { D(i, k) for 1 <= k <= c}。

  对于某一个位置i,暴力的计算D(i)是O(n)的。但是可以发现,对于每一种颜色k, D(i, k)关于i是一个分段函数,而且二次导数和一次导数的变化规律比较容易发现:当i为-inf的时候,D(i, k)的导数为-1, 即i每次向右移动一位(向最近的颜色为k的椅子移动),D(i, k)便减1。当经过第一把颜色为k的椅子时,D(i, k) = 0, 之后便开始上升(远离最近的颜色为k的椅子),导数为1,直到与下一把颜色为k的椅子的距离小于与上一把颜色为k的椅子的距离,D(i, k)又开始按照导数为-1的情况递减。

  画个图就比较容易看出来了,设ii, j为两个相邻颜色为k的椅子的位置,且ii < j, 即a[ii] == a[j] == k, ii < j , for all ii < l < j, a[l] != k。在ii之前D(i, k)导数为-1, 之后变成+1, 所以D(i, k)在ii的二次导数为+2, 当i位于ii和j中间的时候,导数又由+1(possibly 变为0)变为-1,中间可以令ceil((i+j)/2)的二次导数增加-1, 令floor((i+j)/2)的二次导数增加-1。

  在[-inf, 0]时,D(i, k)函数的导数是-1,即每次向正方向移动,距离都会减少1。然后对二次导求一遍前缀和,就可以得到一次导数。这里对于所有的颜色可以统一处理,一次导数的初始值就是-c。然后再计算一下初始位置为0时的答案,对一次导数再求一遍前缀和,就会得到每个位置的D(i)。取最小值即可。

code:

 1 /*
 2  by skydog
 3  */
 4 #include <iostream>
 5 #include <cstdio>
 6 #include <vector>
 7 #include <utility>
 8 #include <algorithm>
 9 #include <cmath>
10 #include <cstring>
11 #include <map>
12 #include <set>
13 #include <stack>
14 #include <queue>
15 #include <deque>
16 #include <cassert>
17 #include <list>
18 using namespace std;
19 typedef long long ll;
20 typedef pair<int, int> ii;
21 typedef pair<ll, ll> l4;
22 
23 #define mp make_pair
24 #define pb push_back
25 ll gcd(ll a, ll b)
26 {
27     return b?gcd(b, a%b):a;
28 }
29 const int maxn = 1e6+10;
30 int n, c, a[maxn], nxt[maxn];
31 ll d[maxn];
32 /*
33  2-delta 1-delta 0-ans
34  0 -c 0ans
35  if encounter a color 2-d += 2
36  if has next, then -1 -1
37  */
38 int main()
39 {
40     scanf("%d %d", &n, &c);
41     for (int i = 1; i <= n; ++i)
42         scanf("%d", a+i);
43     for (int i = n; i >= 1; --i)
44     {
45         d[i] += 2;
46         int &pos = nxt[a[i]];
47         if (pos)
48         {
49             d[(i+pos)/2] -= 1;
50             d[(i+pos+1)/2] -= 1;
51         }
52         pos = i;
53     }
54     for (int i = 1; i <= n; ++i)
55         d[i] += d[i-1];
56     d[0] = -c;
57     for (int i = 1; i <= n; ++i)
58         d[i] += -c;
59     ll start = 0;
60     for (int i = 1; i <= n; ++i)
61     {
62         int &pos = nxt[a[i]];
63         if (pos)
64         {
65             start += i;
66             pos = 0;
67         }
68     }
69     ll ans = 1ll*n*n;
70     for (int i = 1; i <= n; ++i)
71     {
72         start += d[i-1];
73         if (start < ans)
74             ans = start;
75     }
76     ll g = gcd(ans, c);
77     ans /= g;
78     c /= g;
79     printf("%lld/%d
", ans, c);
80 }
View Code

J. Journey

题意:一个数轴上1到n(n<=1e5),每个位置上有一个步长a[i],起初你在1的位置,手里也有一个步长h0,1 <= a[i], h0 <= n-1。每次在一个位置i,你可以选择把自己步长变为a[i],或者不换,继续保持手中的步长。然后你可以向右(即数轴的正方向)移动手中步长单位的距离。问你最后到达位置n有多少种方式,答案mod 998 244 353。

观察

  其实有一个比较明显的dp,或者说模拟,记dp(i, h)表示到达位置i,且手中步长为h的方法数有多少种。每次dp(i, h) 可以更新 dp(i+h, h),或者更新dp(i+a[i], a[i])。

  然后可以发现,dp(i, h) 可以更新dp(i+h, h), dp(i+2*h, h) ... 直到a[i+k*h] 等于h,那么此时dp(i+k*h + h, h) 会变成到达i+k*h位置的方法数之和。即dp(i+k*h, h) == dp(i, h) if a[i+h], ..., a[i+(k-1)*h] 都不等于h,或者说 到这些位置,且手中步长为h的方法数 都相同。

  那么我们便可以从左到右遍历,每次用dp(i, a[i])更新后续的状态,直到遇到某个a[j] == a[i],停止。

  分析时间复杂度,可以想到最坏的情况应该是一开始手中是1,a[1] = a[2] = 2, a[3] = a[4] = a[5] = 3, a[6-9] = 4,..., 总的操作次数是 n + n + n-2 + n - (2+3) + n-(2+3+4) = 2*n + sum of (n - (1+k)*k/2+1)。注意,k最多到O(sqrt(n)),所以整个算法是O(n*sqrt(n))的。其实这里是不是n * sqrt(n) 还需推敲。

code:

 1 /*
 2  by skydog
 3  */
 4 #include <iostream>
 5 #include <cstdio>
 6 #include <vector>
 7 #include <utility>
 8 #include <algorithm>
 9 #include <cmath>
10 #include <cstring>
11 #include <map>
12 #include <set>
13 #include <stack>
14 #include <queue>
15 #include <deque>
16 #include <cassert>
17 #include <list>
18 using namespace std;
19 typedef long long ll;
20 typedef pair<int, int> ii;
21 typedef pair<ll, ll> l4;
22 
23 #define mp make_pair
24 #define pb push_back
25 
26 const int maxn = 1e5+1;
27 const int mod = 998244353;
28 void add(int &a, int b)
29 {
30     a += b;
31     if (a >= mod) a -= mod;
32 }
33 int a[maxn], h, n, d[maxn];
34 void push(int pos, int h)
35 {
36     for (int i = pos+h; i <= n; i += h)
37     {
38         add(d[i], d[pos]);
39         if (a[i] == h)
40             break;
41     }
42 }
43 int main()
44 {
45     scanf("%d %d", &n, &h);
46     for (int i = 1; i <= n; ++i)
47         scanf("%d", a+i);
48     d[1] = 1;
49     if (a[1] != h)
50         push(1, h);
51     for (int i = 1; i < n; ++i)
52         push(i, a[i]);
53     printf("%d
", d[n]);
54 }
View Code

回顾:听室友说他们是用vector把dp状态存下来,然后过了题。为了避免mle,是用了析构的方法(destructor)。貌似就是建立一个局部vector变量,然后将全局的vector和局部的vector swap一下。

原文地址:https://www.cnblogs.com/skyette/p/8433969.html