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 }
回顾:下次可以画个图,整体考虑一下。现场学弟想了一个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 }
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 }
回顾:听室友说他们是用vector把dp状态存下来,然后过了题。为了避免mle,是用了析构的方法(destructor)。貌似就是建立一个局部vector变量,然后将全局的vector和局部的vector swap一下。