Solution Set - 杂题分享
A.[THUPC2018]淘米神的树
先考虑开局只有一个黑点,将黑点做根,问有多少种排列满足父亲在儿子前。很平凡的问题,设\(f_u\)为\(u\)子树的合法序列个数,\(f_u=(siz_u-1)! \sum_{v \in son_u} \frac{f_v}{siz_v!}\),先将根放入,再由合法子树相对序列代替全排列。整理答案为\(ans=\frac{\prod_u (siz_u-1)!}{\prod_{u \neq rt} siz_u!}=\frac{n!}{\prod_u siz_u}\)。
再考虑两个黑点,建一个虚点为根,连上两个黑点,一开始虚点为黑点,一此操作后,a,b为黑点,与原问题等价。现在树成为基环树。枚举删除一条环上的边的答案,当环上的某点作为环上最后一个染黑的点会在断左边边和断右边边中重复出现,答案要乘\(\frac{1}{2}\)。
处理出除环上点的子树大小,给环上点重编号,虚点是0,其他点一次是1~k,设\(a_u\)是环上u点的除了环上儿子的子树大小,\(b_u\)是断掉i->i+1边后的子树大小。有
对a做前缀和为c
可多项式快速插值,时间复杂度\(O(nlog^2n)\)
B.Mergesort Strikes Back
题意
一个长度为\(n\)的随机排列,进行深度为\(k\)的归并排序(\([1\dots n]\)为第一层),求期望逆序对个数。
题解
在每个片段中,合并时的相对顺序保持不变,因此它们所贡献的期望逆序对个数只是\(\frac{l(l-1)}{4}\)(对于长度为l的片段),可以看作枚举两个点,前大于后与后大于前的概率相同,那么前大于后形成逆序对的概率是\(\frac{1}{2}\)。
首先考虑对两个序列归并,找到比前位所有元素的大的元素,一次为分块开头,对每个块以开头元素的大小排序。而对归并k层分成的\(2^{k-1}\)个片段来说,同样。我们只是按照开头的元素对这些块进行排序。让我们考虑由两个不同段中的两个块形成的逆序对。假设形成逆序对的元素是各自片段中的第i个和第j个,考虑这i+j数中的最大值 ,如果它是这两个元素中的一个,就不能形成逆序对。最大值既不是i也不是j的概率是\(\frac{i+j-2}{i+j}\),i和j的大小顺序与两个块开头最大值的大小顺序不同的概率为\(\frac{1}{2}\),那么i,j形成逆序对的概率为\(\frac{i+j-2}{2(i+j)}=\frac{1}{2}-\frac{1}{i+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 \(n ≤ 10^5\).
\(\frac{l(l-1)}{4}\)和\(\frac{1}{2}\)两项都是好求的,只剩下\(\sum_{i<=k} \sum_{j<=k} \sum_{x<=a_i} \sum_{y<=a_j} \frac{1}{x+y} ,k<=n<=1e5\)。
首先一个显然,\(\sum_{i<=k}a_i=n\),然后一个显然,\(a_i\)的取值只有\(O(\sqrt n)\)种。于是我们先对\(\frac{1}{i}\)做一个预处理前缀和,可以省掉一个枚举\(\sum_{y<=a_j}\)。剩下的复杂度\(\sum_{i<=k} \sum_{x<=a_i}\)是\(O(n)\)的,\(\sum_{j<=k}\)是\(O(\sqrt n)\),总复杂度是\(O(n\sqrt n)\).
C.Cutting the Line
D.Expected Earnings
题意
袋子里有\(n\)个球,球有两种:红球和黑球。袋子中刚好有\(k\)个红球的概率为\(p_k\)。每次你珂以从袋子中随机掏出一个球,花费为\(c\)。你每掏出一个红球都会得到\(1\)的收益,掏出黑球没有收益。你珂以随时停止掏球(甚至不掏球)。
求以最优方案操作时获得的最大期望收益。
\(n<=10000\)
题解
数据范围使我们可以\(n^2\)的dp并滚掉第一维。因为我们在前局已定的情况下判断,根据的是之后的收益,所以设\(dp_{i,j}\)表示一共取了i个球,其中j个红球以后的最大收益。在当前局面我们可以选择取或不取,有转移:
\(B_{i,j}\)表示一共取了i个球,其中j个红球,下一次取出红球的概率。通过枚举总共的红球个数,我们有\(O(n^3)\)的式子:
\(P(A|B)\)表示A发生在B条件下的概率,可根据贝叶斯公式\(P(A|B)=\frac{P(A)P(B|A)}{P(B)}\)计算。
现在考虑优化B的计算。这个式子没什么前途,我们给\(B_{i,j}\)一个新的意义:\(B_{i,j}=P(i+1个球中j+1个红球|i个球中j个红球)\),这个看起来就简单很多。再套贝叶斯公式得\(B_{i,j}=\frac{P(i+1个球中j+1个红球)}{P(i个球中j个红球)} \times P(i个球中j个红球|i+1个球中j+1个红球)\),\(P(i个球中j个红球|i+1个球中j+1个红球)\)其实就是最后一个拿的球是红球的概率,为\(\frac{j+1}{i+1}\).
设\(q_{i,j}\)为i个球中j个红球的概率,有转移\(q_{i,j}=q_{i+1,j+1}\frac{j+1}{i+1}+q_{i+1,j}\frac{i+1-j}{i+1}\),可以看作取出i+1个球j+1红球后,拿出一个红球放回袋子里;取出i+1个球j红球后,拿出一个黑球放回袋子里。边界为\(q_{n,i}=p_i\)
点击查看代码
#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上最大的 \(ai \oplus dist(i,v)\),保证u为v的祖先。\(n<=50000\)
题解
从 u 到 v 的路径可以分为 256 个节点的块和(可能)少于 256 个节点的单个块。我们可以通过迭代其所有节点来单独考虑最后一个块。将u点及向上255个点分为一块,设\(f_{u,i}\)表示\(\max_{j \in block_u} (a_j \oplus dis(j,u) \oplus (i<<8))\),对\(a_j \oplus dis(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变成\(\gcd_{i \neq j} a_i \times a_j\),操作的子集总大小不超过\(1e6\)。\(n<=1e5,a_i<=1e6\)。
题解
用lcm表示gcd,可以想到\(Min-Max\)容斥:\(\gcd (S) = \prod_{T \subseteq S} lcm(T)^{(-1)^{|T|+1}}\),操作次数是\(2^{|S|-1}\).
首先想办法将\(|S|\)缩小。随机选取一个数x,对于x的质数次数在序列中可能非最小的,即次数大于0,即x的质因数,最多8个,找到拥有它们的最小次数的数并添加集合。这时集合大小小于等于9.
对于要求gcd,拆开每一位质数考虑,我们选取它两两相加最小,及最小次和次小次相加。考虑最小值是全序列除去非最小值的最小值,次小值是序列除去最小值的最小,找次小值可以枚举删掉序列一个数的最小值减最小值的和再加上最小值。于是将\(\gcd_{i \neq j} a_i \times a_j\)变成\(gcd_i^2 a_i \prod_i \frac{\gcd_{j \neq i}a_j}{\gcd_j a_j}\)。而\(\prod_i \frac{\gcd_{j \neq i}a_j}{\gcd_j a_j}\)不等于1的个数,即可能次小值的个数,同最小值个数,小于等于9个,证明同上。实现时只需要找到拥有最小次的数去掉,记录\(\gcd_j a_j\)作为分母的次数。
子集总大小最大是\(9\times 2^8 \times 9=20736\)。时间复杂度是\(O(n\log V + \omega(A)(n\sqrt n+ 2^{\omega(A)}\omega(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;
}
}