\(\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

\(\mathcal{P}\text{ortal.}\)

闲话:听说这题直接输出 \(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

愿神保佑您。
posted on 2022-07-02 11:48  Oxide  阅读(142)  评论(0编辑  收藏  举报