\(\cal T_2\) 刮痧
Description
这天 Colin 教 Eva 打隔膜,考虑到 Eva 胆子很小,Colin 决定教她刮痧流打法。
Eva 使用的英雄是 Scraping Queen,每次攻击可以使选定的目标生命值 \(−1\),可以攻击 \(m\) 次。教程关一共有 \(n\) 个怪物,第 \(i\) 个生命值为 \(a_i\),都没有攻击力,排成一排等着挨打。
虽然怪物不会进攻,但是长相实在是太恐怖了,Eva 还是很害怕,只敢闭上眼乱砍。所以每次行动她会随机选一个当前生命值 \(> 0\) 的怪物攻击。
现在 Colin 想知道,经过 \(m\) 次攻击后,Eva 能击败的怪物期望数。称一个怪物被击败,当且仅当某一时刻其生命值等于零。
\(n\leqslant 15,1\leqslant m,a_i\leqslant 100,\sum a_i\geqslant m\).
Solution
\(\bold{Warning}\):题解复读机。
首先可以想出一个关于怪物血量的 \(\mathtt{dp}\),但这需要优化:可以想到,如果我们已经知道最后哪些怪物会死,就不需要记录下怪物的血量,只需要在打怪的时候乘上打若干次某怪的系数即可。
记 \(g(i,S)\) 为对 \(j\in S\) 的怪物一共砍 \(i\) 刀且都不砍死的方案数,每次往集合中加怪需要 \(\mathcal O(m^2)\) 的 \(\mathtt{dp}\),所以是 \(\mathcal O(2^nm^2)\) 的。
记 \(f(i,S)\) 为砍 \(i\) 刀使得 \(j\in S\) 的怪物都死亡的概率(不计算没有砍死的怪物),\(\text{sum}(S)\) 为集合 \(S\) 中怪物的血量和。那么第 \(i+1\) 刀有以下情况:
- 砍死一只怪,设其为 \(j\)。那么 \(i-\text{sum}(S)\) 中有 \(a_j-1\) 次砍在 \(j\) 怪上,贡献是 \(\displaystyle \dbinom{i-\text{sum}(S)}{a_j-1}\big /|U\setminus S|\);
- 没砍死。贡献是 \(1\big /|U\setminus S|\).
这个复杂度是 \(\mathcal O(2^nnm)\) 的。总复杂度 \(\mathcal O(2^nm(n+m))\).
Code
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp]=x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
# include <iostream>
using namespace std;
const int maxn = 1<<15;
int n, m, a[20], sum[maxn], lg[maxn];
long double f[105][maxn], C[105][105], g[105][maxn];
int lowbit(int x) { return x&-x; }
int main() {
freopen("scraping.in","r",stdin);
freopen("scraping.out","w",stdout);
n=read(9), m=read(9); int lim=1<<n;
for(int i=1;i<=n;++i) a[i]=read(9); lg[0]=-1;
for(int i=1;i<lim;++i) {
lg[i] = lg[i>>1]+1;
sum[i] = sum[i^lowbit(i)]+a[lg[lowbit(i)]+1];
} C[0][0]=1;
for(int i=1;i<=m;++i) {
C[i][0]=1;
for(int j=1;j<=i;++j)
C[i][j] = C[i-1][j]+C[i-1][j-1];
} g[0][0]=1;
for(int s=1;s<lim;++s) {
int t = s^lowbit(s), k=lg[lowbit(s)]+1;
for(int i=m;i>=0;--i)
for(int j=min(a[k]-1,i);j>=0;--j)
g[i][s] += g[i-j][t]*C[i][j];
} long double ans=0; f[0][0]=1;
for(int s=0;s<lim-1;++s) {
int cnt = n-__builtin_popcount(s);
for(int i=sum[s];i<m;++i) {
for(int j=1;j<=n;++j) if(!(s>>j-1&1))
f[i+1][s|(1<<j-1)] += f[i][s]*C[i-sum[s]][a[j]-1]/cnt;
f[i+1][s] += f[i][s]/cnt;
}
}
for(int s=0;s<lim;++s) if(m>=sum[s]) {
int t = (lim-1)^s, cnt=__builtin_popcount(s);
ans += f[m][s]*g[m-sum[s]][t]*cnt;
} printf("%.5Lf\n",ans);
return 0;
}
\(\cal T_3\) 感染
Description
闲话:听说这题直接输出 \(n\) 有 \(\text{40 pts}\),感觉自己还是胆子不够大啊(?
Solution
\(\color{black}{\bold{Warning}}\):没有代码实现,没有代码实现,没有代码实现。
首先固定母体的方向,基于此每个点的方向实际是固定的。首先可以确定的是每个点都一定会向母体的方向走 —— 这样过于宽泛。事实上,假设母体初始坐标为 \((a,b)\),记点 \((p,q)\) 与 \(x=a,y=b\) 这两条直线的距离分别为 \(A,B\),当 \(A\leqslant B\) 时,一定选向母体的竖直方向;反之选向母体的水平方向。
现在考虑什么样的点 \(x\) 可能会被点 \(y\) 直接 传染。要么 \(x,y\) 相互在彼此的方向中,要么 \(x,y\) 形成的直线斜率为 \(1/-1\)(注意还需要判断一下方向)。于是可以考虑用 \(\rm dijkstra\) 来完成拓展,复杂度应当是 \(\mathcal O(n^2\log n)\) 的?
考虑优化建图,可以给每个点建各个方向的虚点,把在一条平行于坐标轴/斜率为 \(1/-1\) 的直线上的点相邻进行连边,权值为距离。那么每个点只需要对各个方向连一个最近的虚点即可,复杂度 \(\mathcal O(n\log n)\).
Code
愿神保佑您。