【题解】PKUWC2018简要题解

Minimax

先离散化一下所有叶子节点的值,然后考虑一个DP

\(dp(i,j)\)表示在\(i\)点权值是第\(j\)小的概率,只有一个儿子的时候直接转移过来,转移很显然就不写了

然后考虑线段树合并维护这个过程,因为转移时是乘上一段后缀/前缀,同时线段树合并可以获得好的效果。

考虑一下时间复杂度,就是线段树合并的复杂度\(O(n\log n)\)

//@winlere
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define mid ((l+r)>>1)
#define pp(pos) {seg[pos].val=MOD(seg[seg[pos].ls].val+seg[seg[pos].rs].val);}

using namespace std;  typedef long long ll;   char __buf[1<<22],*__c=__buf;
inline int qr(){
    int ret=0,f=0,c=*__c++;
    while(!isdigit(c))f|=c==45,c=*__c++;
    while(isdigit(c)) ret=ret*10+c-48,c=*__c++;
    return f?-ret:ret;
}

const int mod=998244353;
const int maxn=3e5+5;
inline int ksm(const int&ba,const int&p){
    int ret=1;
    for(int t=p,b=ba%mod;t;t>>=1,b=1ll*b*b%mod)
        if(t&1) ret=1ll*ret*b%mod;
    return ret;
}
const int baseinv=ksm(1e4,mod-2);

int e[maxn][2];
struct E{
    int ls,rs,val,tag;
    E(const int&a=0,const int&b=0,const int&c=0,const int&d=1):ls(a),rs(b),val(c),tag(d){}
    inline E&operator *=(ll a){return *this=E(ls,rs,val*a%mod,tag*a%mod);}
}*seg;

inline void pd(const int&pos){
    if(seg[pos].tag==1) return;
    seg[seg[pos].ls]*=seg[pos].tag;
    seg[seg[pos].rs]*=seg[pos].tag;
    seg[pos].tag=1;
}
inline int MOD(const int&x){return x>mod?x-mod:x;}
int rt[maxn],n,w[maxn],sav[maxn],ans,cnt,len;
struct vec{
    int data[2];
    inline int&operator[](int x){return data[x];}
};

//p0=ch max p1=ch min
int Merge(vec t,vec pre,vec suf,vec p){
    if(!t[0]||!t[1]){
        register int k=t[1]>0;
        seg[t[k]]*=1ll*p[0]*pre[k^1]%mod+1ll*p[1]*suf[k^1]%mod;
        return t[k];
    }
    pd(t[0]); pd(t[1]);
    vec P=pre,S=suf;
    for(int i=0;i<=1;++i)
        pre[i]=MOD(pre[i]+seg[seg[t[i]].ls].val),suf[i]=MOD(suf[i]+seg[seg[t[i]].rs].val);
    seg[t[0]].ls=Merge({seg[t[0]].ls,seg[t[1]].ls},P,suf,p);
    seg[t[0]].rs=Merge({seg[t[0]].rs,seg[t[1]].rs},pre,S,p);
    pp(t[0]);
    return t[0];
}

void build(int p,int l,int r,int&pos){
    if(p<l||p>r) return;
    if(!pos) seg[pos=++cnt]=E();
    if(l==r){seg[pos].val=1;return;}
    if(p<=mid) build(p,l,mid,seg[pos].ls);
    else build(p,mid+1,r,seg[pos].rs);
    pp(pos);
}

void dfs(const int&now){
    if(!e[now][0]) return build(w[now],1,len,rt[now]);
    if(!e[now][1]) return dfs(e[now][0]),rt[now]=rt[e[now][0]],void();
    dfs(e[now][0]); dfs(e[now][1]);
    rt[now]=Merge({rt[e[now][0]],rt[e[now][1]]},{0,0},{0,0},{w[now],mod-w[now]+1});
}

void que(int l,int r,int pos){
    if(l==r){ans=(ans+1ll*seg[pos].val*seg[pos].val%mod*l%mod*sav[l]%mod)%mod;return;}
    pd(pos);
    que(l,mid,seg[pos].ls),que(mid+1,r,seg[pos].rs);
}

int main(){
    fread(__c=__buf,1,1<<22,stdin);
    n=qr(); qr();
    seg=(E*)malloc(sizeof(E)*n*18.2);
    for(int t=2,a;t<=n;++t)
        a=qr(),(e[a][0]?e[a][1]:e[a][0])=t;
    for(int t=1;t<=n;++t){
        w[t]=qr();
        if(e[t][0]) w[t]=1ll*w[t]*baseinv%mod;
        else sav[++len]=w[t];
    }
    sort(sav+1,sav+len+1);
    for(int t=1;t<=n;++t)
        if(!e[t][0])
            w[t]=lower_bound(sav+1,sav+len+1,w[t])-sav;
    dfs(1);
    que(1,len,1);
    printf("%d\n",ans);
    return 0;
}

Slay the Spire

由于强化牌\(>1\)所以能打强化牌就打强化牌,最后剩下一个打攻击牌。很显然打攻击牌是打那些选出\(m\)个最大的那几个,那么将攻击牌序列排序,可能打的攻击牌是一个子序列。那么设\(dp(i,j)\)表示钦定\(i\)选中,选了\(j\)个的权值和,转移显然,要记前缀和。

那么如何处理强化牌呢?可以考虑到,强化牌最多可以打\(k-1\)张,再多是没有意义的,同样排序后,设\(f(i,j)\)表示选了钦定选择了\(i\)号牌,总共选择了\(j\)个牌的强化权值,(要记前缀和)。为了满足上面这个\(\le k-1\)的限制,我们钦定当强化牌数量\(> k-1\)的时候,接下来加入强化牌的权值都为1。这样,多选的强化牌就不会造成伤害的贡献,但是仍然可以贡献方案。

//@winlere
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<assert.h>

using namespace std;  typedef long long ll;
int qr(){
    int ret=0,f=0,c=getchar();
    while(!isdigit(c))f|=c==45,c=getchar();
    while(isdigit(c)) ret=ret*10+c-48,c=getchar();
    return f?-ret:ret;
}

const int mod=998244353;
const int maxn=3e3+15;
int st[maxn],w[maxn];
int dp[maxn][maxn];
int f[maxn][maxn];
int c[maxn][maxn];
int n,m,k,ans;

int MOD(const int&x){return x>=mod?x-mod:x;}
int MOD(const int&x,const int&y){return 1ll*x*y%mod;}

int main(){
    n=3e3;
    c[0][0]=1;
    for(int t=1;t<=n;++t){
        c[t][0]=1;
        for(int i=1;i<=t;++i){
            c[t][i]=MOD(c[t-1][i]+c[t-1][i-1]);
        }
    }
    int T=qr();
    while(T--){
        n=qr(); m=qr(); k=qr();
        for(int t=1;t<=n;++t) st[t]=qr();
        for(int t=1;t<=n;++t) w[t]=qr();
        sort(st+1,st+n+1,greater<int>());
        sort(w+1,w+n+1,greater<int>());
        ans=0;
        f[0][0]=1;
        for(int t=1;t<=n;++t){
            dp[t][0]=dp[t-1][0];
            f[t][0]=f[t-1][0];
            for(int i=t;i;--i)
                dp[t][i]=MOD(dp[t-1][i]+MOD(dp[t-1][i-1]+MOD(c[t-1][i-1],w[t])));
            for(int i=1;i<=t;++i)
                f[t][i]=MOD(f[t-1][i]+MOD(f[t-1][i-1],i<k?st[t]:1));
        }
        for(int t=min(n,m);~t;--t){
            int s=0,Cash=m-t,Pay=max(k-t,1);
            for(int i=t;i<=n;++i) s=MOD(s+f[i][t]-(i?f[i-1][t]:0)+mod);
            if(Cash>n||!s) continue;
            for(int i=1;i<=n;++i)
                if(n-i>=Cash-Pay)
                    ans=MOD(ans+MOD(s,MOD(c[n-i][Cash-Pay],dp[i][Pay]-dp[i-1][Pay]+mod)));
        }
        printf("%d\n",ans);
    }
    return 0;
}

斗地主

随机算法

\(dp(S)\)表示\(S\)集合是排列中前\(|S|\)个元素时,按上述算法在所有可能的顺序下能够算出独立集的大小\(=Max(S)\)的概率,Max(S)字面意思,所有可能中最大的独立集大小。那么考虑,枚举排列第一个元素是谁设为u,然后访问去除所有和\(u\)相连的点 的状态\(S'\),那么贡献就是\(dp(S')\over popcnt(S)\) 。注意只能从所有\(Max(S')+1=Max(S)\)的状态转移来。

钦定统计第一个!!!!太有技巧性了

//@winlere
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define lb(t) ((t)&-(t))
using namespace std;  typedef long long ll;
inline int qr(){
    int ret=0,f=0,c=getchar();
    while(!isdigit(c))f|=c==45,c=getchar();
    while(isdigit(c)) ret=ret*10+c-48,c=getchar();
    return f?-ret:ret;
}
const int maxn=20;
const int mod=998244353;
int e[maxn],n,m,u;
int dp[1<<maxn],Max[1<<maxn];
int id[1<<maxn],num[1<<maxn],invc[21],ans;

inline int ksm(const int&ba,const int&p){
    int ret=1;
    for(int t=p,b=ba;t;t>>=1,b=1ll*b*b%mod)
        if(t&1) ret=1ll*ret*b%mod;
    return ret;
}
inline int MOD(const int&x){return x>=mod?x-mod:x;}

int main(){

    n=qr(); m=qr(); u=(1<<n)-1;
    for(int t=1;t<=n;++t) invc[t]=ksm(t,mod-2);
    for(int t=1,a,b;t<=m;++t)
        a=qr()-1,b=qr()-1,e[a]|=1<<b,e[b]|=1<<a;
    for(int t=0;t<n;++t) id[1<<t]=t;
    for(int t=1;t<=u;++t) num[t]=num[t^lb(t)]+1;
    dp[0]=1;Max[0]=0;
    for(int t=1;t<=u;++t){
        int temp=t;
        while(temp){
            int f=lb(temp),g=id[f],k=e[g]^u,s=(t&k)^f;
            temp^=f;
            if(Max[s]+1>Max[t]) dp[t]=dp[s],Max[t]=Max[s]+1;
            else if(Max[s]+1==Max[t]) dp[t]=MOD(dp[t]+dp[s]);
        }
        dp[t]=1ll*dp[t]*invc[num[t]]%mod;
    }
    printf("%d\n",dp[u]);
    return 0;
}

猎人杀

\(dp(S)\)表示钦定\(S\)中的人在\(1\)号猎人之后死亡,其他人生死我们不管。可以知道在任何时刻,死掉\(S\)中的任何一个人的概率始终是死掉\(1\)号猎人的\(w_{ x\in S}\over w_1\)倍。注意\(1\not \in S\) 那么
\[dp(S)={w_1\over w_{ x\in S}+w_1}\]
那么考虑题目给我们的条件,是要求\(1\)是最后一个死亡的概率,也就是不存在有人在他后面死亡。由于\(dp(S)\)是钦定,考虑容斥,答案就是
\[\sum_{S\subseteq U-1} (-1)^{|S|+1} dp(S)\]
然后用背包优化容斥即可

我们对着\(w_{x\in S}\)带容斥系数进行背包,设\(dp(i)\)表示\(w_{x \in S}=i\)的方案数,根据\(|S|\)的不同带符号,那么他的生成函数是
\[ [x^i]\prod (1-x^{w_j})\]
由于多项式乘法满足结合律也满足交换律,所以直接二进制合并即可,复杂度\(O(n \log^2 n)\)

猫猫说这做到的是最优复杂度。

//@winlere
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#include<assert.h>
#include<cmath>

using namespace std;  typedef long long ll;
inline int qr(){
    int ret=0,f=0,c=getchar();
    while(!isdigit(c))f|=c==45,c=getchar();
    while(isdigit(c)) ret=ret*10+c-48,c=getchar();
    return f?-ret:ret;
}
const int maxn=1e5+5;
const int mod=998244353;
int w[maxn],inv[maxn],n,s,ans;
inline int MOD(const int&x){return x>=mod?x-mod:x;}
inline int MOD(const int&x,const int&y){return 1ll*x*y%mod;}
inline int ksm(const int&ba,const int&p){
    int ret=1;
    for(int t=p,b=ba%mod;t;t>>=1,b=1ll*b*b%mod)
        if(t&1) ret=1ll*ret*b%mod;
    return ret;
}

namespace poly{
    typedef vector<int> poly;
    const int maxn=1<<18;
    const int g=3;
    const int gi=ksm(g,mod-2);
    int r[maxn];
    inline void getr(const int&len){
        static int L=0;
        if(L==len) return;
        L=len;
        for(int t=0;t<len;++t)
            r[t]=r[t>>1]>>1|(t&1?L>>1:0);
    }
    inline void NTT(poly&a,const int&len,const int&tag){
        getr(len);
        for(int t=0;t<len;++t)
            if(t<r[t]) swap(a[t],a[r[t]]);
        int s=tag==1?g:gi;
        for(int t=1,wn;t<len;t<<=1){
            wn=ksm(s,(mod-1)/(t<<1));
            for(int i=0;i<len;i+=t<<1)
                for(int j=0,w=1,k;j<t;++j,w=1ll*wn*w%mod)
                    k=1ll*a[t+i+j]*w%mod,a[t+i+j]=MOD(a[i+j]-k+mod),a[i+j]=MOD(a[i+j]+k);
        }
        if(tag!=1)
            for(int t=0,i=ksm(len,mod-2);t<len;++t)
                a[t]=1ll*a[t]*i%mod;
    }
    inline int foo(const int&len){
        int ret=1;
        while(ret<len) ret<<=1;
        return ret;
    }
    queue<int> q;
    inline poly DIVD_NTT(vector<poly>a){
        for(auto&t:a) t.resize(foo(t.size()));
        for(int i=0,ed=a.size();i<ed;++i) q.push(i);
        while(q.size()>1){
            int t1=q.front();
            q.pop();
            int t2=q.front();
            q.pop();
            int g=a[t1].size()+a[t2].size(),k=foo(a[t1].size()+a[t2].size()-1);
            a[t1].resize(k); a[t2].resize(k);
            NTT(a[t1],k,1); NTT(a[t2],k,1);
            for(int t=0;t<k;++t) a[t1][t]=1ll*a[t1][t]*a[t2][t]%mod;
            NTT(a[t1],k,0);
            a[t1].resize(g);
            q.push(t1);
        }
        return a[q.front()];
    }
}

vector<int> dp;
vector<poly::poly> a;
int main(){
    n=qr();
    for(int t=1;t<=n;++t) w[t]=qr(),s+=w[t];
    sort(w+2,w+n+1);
    for(int t=2;t<=n;++t)
        a.push_back(vector<int>(w[t]+1)),a.back()[0]=1,a.back()[w[t]]=mod-1;
    dp=poly::DIVD_NTT(a);
    inv[1]=1;
    for(int t=2;t<=s;++t) inv[t]=1ll*(mod-mod/t)*inv[mod%t]%mod;
    for(int t=0,ed=dp.size();t<ed;++t) ans=MOD(ans+1ll*dp[t]*w[1]%mod*inv[w[1]+t]%mod);
    printf("%d\n",ans);
    return 0;
}

随机游走

不会

12-23 23:54