Solution Set - 杂题分享

A.[THUPC2018]淘米神的树

先考虑开局只有一个黑点,将黑点做根,问有多少种排列满足父亲在儿子前。很平凡的问题,设fuu子树的合法序列个数,fu=(sizu1)!vsonufvsizv!,先将根放入,再由合法子树相对序列代替全排列。整理答案为ans=u(sizu1)!urtsizu!=n!usizu

再考虑两个黑点,建一个虚点为根,连上两个黑点,一开始虚点为黑点,一此操作后,a,b为黑点,与原问题等价。现在树成为基环树。枚举删除一条环上的边的答案,当环上的某点作为环上最后一个染黑的点会在断左边边和断右边边中重复出现,答案要乘12

处理出除环上点的子树大小,给环上点重编号,虚点是0,其他点一次是1~k,设au是环上u点的除了环上儿子的子树大小,bu是断掉i->i+1边后的子树大小。有

bu={j=1uaj1uij=ukaji+1uk

对a做前缀和为c

ubu=u=0,ui|cuci|

可多项式快速插值,时间复杂度O(nlog2n)

B.Mergesort Strikes Back

题意

一个长度为n的随机排列,进行深度为k的归并排序([1n]为第一层),求期望逆序对个数。

题解

在每个片段中,合并时的相对顺序保持不变,因此它们所贡献的期望逆序对个数只是l(l1)4(对于长度为l的片段),可以看作枚举两个点,前大于后与后大于前的概率相同,那么前大于后形成逆序对的概率是12

首先考虑对两个序列归并,找到比前位所有元素的大的元素,一次为分块开头,对每个块以开头元素的大小排序。而对归并k层分成的2k1个片段来说,同样。我们只是按照开头的元素对这些块进行排序。让我们考虑由两个不同段中的两个块形成的逆序对。假设形成逆序对的元素是各自片段中的第i个和第j个,考虑这i+j数中的最大值 ,如果它是这两个元素中的一个,就不能形成逆序对。最大值既不是i也不是j的概率是i+j2i+j,i和j的大小顺序与两个块开头最大值的大小顺序不同的概率为12,那么i,j形成逆序对的概率为i+j22(i+j)=121i+j

再预处理一些东西,可以在O(n)的时间计算两个片段的答案,又有结论片段的长度取值最多2个,答案又只与段长对有关,所以全部答案可得。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAX=2e5+10;
#define int long long
int n,k,mod,x,y,cnt[MAX],f[MAX],ans,inv;vector<int>v;
inline int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
inline int power(int a,int b){
    int res=1;
    while(b){
        if(b&1)  res=res*a%mod;
        a=a*a%mod;b>>=1;
    }return res;
}
void solve(int l,int r,int k){
    if(k==1){v.push_back(r-l+1);return;}
    int mid=l+r>>1;
    solve(l,mid,k-1);solve(mid+1,r,k-1);
}inline int work(int x,int y){
    int res=x*y%mod*inv%mod;
    for(int i=1;i<=y;++i)  res=(res-f[i+x]+f[i])%mod;
    return res;
}
signed main(){
    n=read();k=read();mod=read();inv=mod+1>>1;
    solve(1,n,k);
    x=v[0];
    for(auto i:v){
        if(i!=x){
            y=i;cnt[y]++;
        }else  cnt[x]++;
    }ans=x*(x-1)%mod*inv%mod*inv%mod*cnt[x]%mod;
    ans=(ans+y*(y-1)%mod*inv%mod*inv%mod*cnt[y]%mod)%mod;
    for(int i=1;i<=n*2;++i)  f[i]=(f[i-1]+power(i,mod-2))%mod;
    ans=(ans+work(x,x)*cnt[x]%mod*(cnt[x]-1)%mod*inv)%mod;
    ans=(ans+work(x,y)*cnt[x]%mod*cnt[y])%mod;
    ans=(ans+work(y,y)*cnt[y]%mod*(cnt[y]-1)%mod*inv)%mod;
    ans=(ans+mod)%mod;
    cout<<ans;
}

Bonus: solve this problem when the segments' length can be arbitary and n105.

l(l1)412两项都是好求的,只剩下i<=kj<=kx<=aiy<=aj1x+yk<=n<=1e5

首先一个显然,i<=kai=n,然后一个显然,ai的取值只有O(n)种。于是我们先对1i做一个预处理前缀和,可以省掉一个枚举y<=aj。剩下的复杂度i<=kx<=aiO(n)的,j<=kO(n),总复杂度是O(nn).

C.Cutting the Line

D.Expected Earnings

题意

袋子里有n个球,球有两种:红球和黑球。袋子中刚好有k个红球的概率为pk。每次你珂以从袋子中随机掏出一个球,花费为c。你每掏出一个红球都会得到1的收益,掏出黑球没有收益。你珂以随时停止掏球(甚至不掏球)。

求以最优方案操作时获得的最大期望收益。

n<=10000

题解

数据范围使我们可以n2的dp并滚掉第一维。因为我们在前局已定的情况下判断,根据的是之后的收益,所以设dpi,j表示一共取了i个球,其中j个红球以后的最大收益。在当前局面我们可以选择取或不取,有转移:

dpi,j=max(0,Bi,j(dpi+1,j+1+1)+(1Bi,j)dpi+1,jc)

Bi,j表示一共取了i个球,其中j个红球,下一次取出红球的概率。通过枚举总共的红球个数,我们有O(n3)的式子:

Bi,j=k>jni+jP(k|ij)×kjni

P(A|B)表示A发生在B条件下的概率,可根据贝叶斯公式P(A|B)=P(A)P(B|A)P(B)计算。

现在考虑优化B的计算。这个式子没什么前途,我们给Bi,j一个新的意义:Bi,j=P(i+1j+1|ij),这个看起来就简单很多。再套贝叶斯公式得Bi,j=P(i+1j+1)P(ij)×P(ij|i+1j+1),P(ij|i+1j+1)其实就是最后一个拿的球是红球的概率,为j+1i+1.

qi,j为i个球中j个红球的概率,有转移qi,j=qi+1,j+1j+1i+1+qi+1,ji+1ji+1,可以看作取出i+1个球j+1红球后,拿出一个红球放回袋子里;取出i+1个球j红球后,拿出一个黑球放回袋子里。边界为qn,i=pi

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAX=10010;
const double eps=1e-18;
int n,p;double c,q[2][MAX],dp[2][MAX],P;
inline int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int main(){
    n=read();c=read()*0.000001;
    for(int i=0;i<=n;++i)  q[p][i]=read()*0.000001;
    for(int i=n-1;i>=0;--i){
        p^=1;memset(dp[p],0,sizeof(dp[p]));
        memset(q[p],0,sizeof(q[p]));
        for(int j=0;j<=i;++j){
            q[p][j]=q[p^1][j+1]*(j+1)/(i+1)+q[p^1][j]*(i+1-j)/(i+1);
            if(q[p][j]<eps)  continue;
            P=q[p^1][j+1]*(j+1)/q[p][j]/(i+1);
            dp[p][j]=max(0.0,P*(dp[p^1][j+1]+1)+(1-P)*(dp[p^1][j])-c);
            // cout<<i<<' '<<j<<" "<<dp[p][j]<<endl;
        }
    }printf("%.10f",dp[p][0]);
}

E.In a Trap

怎么我找到的题都这么简单啊T_T

题意

有一颗以1为根的树,每个点上有一个点权ai,每次询问路径u到v上最大的 aidist(i,v),保证u为v的祖先。n<=50000

题解

从 u 到 v 的路径可以分为 256 个节点的块和(可能)少于 256 个节点的单个块。我们可以通过迭代其所有节点来单独考虑最后一个块。将u点及向上255个点分为一块,设fu,i表示maxjblocku(ajdis(j,u)(i<<8)),对ajdis(j,u)建trie树,询问i<<8就行。

怎么水了

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAX=50010;
int n,q,x,y,a[MAX],f[MAX][300],dep[MAX],fa[MAX],tr[1<<20][2],vis[1<<20],las[MAX],tot,head[MAX];
struct edge{
    int t,nxt;
} e[MAX<<1];
inline int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
inline void add(int x,int y){
	for(int i=16,p=0;i>=0;--i){
		int u=(x>>i)&1;
		if(!tr[p][u])  tr[p][u]=++tot;
		p=tr[p][u];vis[p]=y;
	}
}inline int ask(int x,int y){
	int res=0;
	for(int i=16,p=0;i>=0;--i){
		int u=(x>>i)&1;
		if(vis[tr[p][u^1]]==y){
			res|=1<<i;p=tr[p][u^1];
		}else  p=tr[p][u];
	}return res;
}
void dfs(int u,int dad){
	fa[u]=dad;dep[u]=dep[dad]+1;
	if(dep[u]>=256){
        int i;
		for(i=u;dep[u]-dep[i]<256;i=fa[i])
			add(a[i]^(dep[u]-dep[i]),u);
            las[u]=i;
		for(int i=0;i<256;++i)  f[u][i]=ask(i<<8,u);
	}for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].t;
		if(v==dad)  continue;
		dfs(v,u);
	}
}
int main(){
	n=read();q=read();
	for(int i=1;i<=n;++i)  a[i]=read();
	for(int i=1;i<n;++i){
		x=read();y=read();
		e[++tot]={y,head[x]};head[x]=tot;
		e[++tot]={x,head[y]};head[y]=tot;
	}tot=0;dfs(1,0);
	while(q--){
		x=read();y=read();int ans=0,dis=0;
		while(dep[y]-dep[x]>=256){
			ans=max(ans,f[y][dis]);++dis;y=las[y];
		}dis<<=8;
		while(y!=fa[x]){
			ans=max(ans,a[y]^dis);++dis;y=fa[y];
		}printf("%d\n",ans);
	}
}

F.Become Big For Me

题意

有序列a,通过乘除序列子集的lcm,将1变成gcdijai×aj,操作的子集总大小不超过1e6n<=1e5,ai<=1e6

题解

用lcm表示gcd,可以想到MinMax容斥:gcd(S)=TSlcm(T)(1)|T|+1,操作次数是2|S|1.

首先想办法将|S|缩小。随机选取一个数x,对于x的质数次数在序列中可能非最小的,即次数大于0,即x的质因数,最多8个,找到拥有它们的最小次数的数并添加集合。这时集合大小小于等于9.

对于要求gcd,拆开每一位质数考虑,我们选取它两两相加最小,及最小次和次小次相加。考虑最小值是全序列除去非最小值的最小值,次小值是序列除去最小值的最小,找次小值可以枚举删掉序列一个数的最小值减最小值的和再加上最小值。于是将gcdijai×aj变成gcdi2aiigcdjiajgcdjaj。而igcdjiajgcdjaj不等于1的个数,即可能次小值的个数,同最小值个数,小于等于9个,证明同上。实现时只需要找到拥有最小次的数去掉,记录gcdjaj作为分母的次数。

子集总大小最大是9×28×9=20736。时间复杂度是O(nlogV+ω(A)(nn+2ω(A)ω(A)))

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAX=1e5+10,V=1e6;
// #define int long long
int n,a[MAX],G,pre[MAX],nxt[MAX],cur,cnt;
vector<int>c;vector<pair<int,vector<int> > >ans;
inline int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int gcd(int a,int b){
    if(!b)  return a;
    return gcd(b,a%b);
}
inline void solve(vector<int>res,int op){
    int n=res.size();vector<int>a;op^=1;
    for(int i=1;i<1<<n;++i){
        a.clear();
        for(int j=0;j<n;++j)
            if((i>>j)&1)  a.push_back(res[j]);
        ans.push_back({(__builtin_popcount(i)&1)^op,a});
    }
}
inline vector<int>work(int x,int G){
    vector<int>res;int Q=a[x%n+1];
    res.push_back(x%n+1);
    for(int i=2;i<=V;++i){
        int w1=1,w2=1;
        while(G%i==0)  w1*=i,G/=i;
        while(Q%i==0)  w2*=i,Q/=i;
        if(w1==w2)  continue;
        for(int j=1;j<=n;++j)
            if(j!=x&&(a[j]/w1)%i){
                Q=gcd(Q,a[j]);res.push_back(j);break;
            }
    }return res;
}
signed main(){
    n=read();
    for(int i=1;i<=n;++i)  a[i]=read();
    for(int i=1;i<=n;++i)  pre[i]=gcd(pre[i-1],a[i]);
    for(int i=n;i;--i)  nxt[i]=gcd(nxt[i+1],a[i]);
    G=pre[n];cnt=2;
    for(int i=1;i<=n;++i){
        cur=gcd(pre[i-1],nxt[i+1]);
        // cout<<i<<" "<<cur<<" "<<G<<endl;
        if(cur^G){
            solve(work(i,cur),0);--cnt;
        }
    }c=work(0,G);
    while(cnt>0)  --cnt,solve(c,0);
    while(cnt<0)  ++cnt,solve(c,1);
    printf("%ld\n",ans.size());
    for(auto i:ans){
        printf("%d %ld ",i.first,i.second.size());
        sort(i.second.begin(),i.second.end());
        for(auto j:i.second)  printf("%d ",j);
        cout<<endl;
    }
}
posted @   yisiwunian  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示