【题解】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;
}
随机游走
不会