====== CCPC Wannafly Camp Day1 ====== [[https://ac.nowcoder.com/acm/contest/3979|比赛链接]] ===== A. 期望逆序对 ===== ==== 题意 ==== 给定 $n$ 个随机变量,第 $i$ 个变量的取值为 $[l_i,r_i]$ 中的随机一个数。 要求将 $n$ 个随机变量重新排列,使得最终的逆序对的期望值最小。询问该方案下逆序对的期望值。 ==== 题解 ==== 假设最终排序为 $p_1p_2\cdots p_n$,考虑相邻两个随机变量 $p_i$ 和 $p_{i+1}$,显然 $p_i$ 大于 $p_{i+1}$ 的概率不超过 $\frac 12$ 时最优。 事实上,这等价于 $l_i+r_i\le l_{i+1}r_{i+1}$,发现不等号两边的式子只与自身有关。 于是该性质具有传递性,于是对不相邻的随机变量也应该满足该约束,对序列按上述关键字排序即可得到最终序列。 考虑暴力计算最终答案,枚举数对 $i\lt j$,该数对的贡献为 $\sum_{k=l_i}^{r_i}\max(0,\min(k-l_j,r_j-l_j+1))$,可以 $O(1)$ 计算。 时间复杂度 $O(n^2)$。 const int MAXN=5e3+5,Mod=998244353; int Inv[MAXN]; int inv(int x){ int k=Mod-2,ans=1; while(k){ if(k&1)ans=1LL*ans*x%Mod; x=1LL*x*x%Mod; k>>=1; } return ans; } struct Seg{ int lef,rig; bool operator < (const Seg &b)const{ return lef+rigr2) s=1LL*(r1-r2)*(r2-l2+1)%Mod; s=(s+1LL*(R-l2+L-l2)*(R-L+1)/2)%Mod; return s; } int main() { int n=read_int(); _for(i,0,n) seg[i].lef=read_int(),seg[i].rig=read_int(); sort(seg,seg+n); int ans=0; _for(i,0,n) Inv[i]=inv(seg[i].rig-seg[i].lef+1); _for(i,0,n) _for(j,i+1,n) ans=(ans+1LL*Inv[i]*Inv[j]%Mod*cal(seg[i].lef,seg[i].rig,seg[j].lef,seg[j].rig))%Mod; enter(ans); return 0; } ===== E. 树与路径 ===== ==== 题意 ==== 给定一个有根树,定义任意两点 $u,v$ 的路径为 $\text{dis}(u,p)\text{dis}(v,p)(p=\text{LCP}(u,v))$。 给定 $m$ 条路径,询问以每个结点为跟时所有路径的权值和。 ==== 题解 ==== 先考虑每条路径对以路径上每个结点为根节点时答案的贡献,记 $\text{dis}(u,v)=L$。 对 $u\to p$ 上路径的每个结点,不难得出该路径对某个结点 $i$ 的贡献为 $(d_u-d_i)(L-d_u+d_i)$。 而对路径上的其他结点,该路径对该点的贡献等于该路径对路径上距离该点最近的结点的贡献。 于是可以树上差分维护该路径对所有点的贡献,其中路径 $u\to p(\text{不含} p)$ 的点的差分值为 $2(d_u-d_i)+1-L$,同理 $v\to p(\text{不含} p)$ 也类似。 而其他点差分值为 $0$。发现差分值为等差数列,于是考虑利用子树和维护路径 $u\to p(\text{不含} p)$ 的差分值。 注意子树和维护最后结点 $p$ 的差分值为 $2(d_u-d_p)+1-L+2(d_v-d_p)+1-L=2$,而 $p$ 的实际差分值应该为 $0$,需要修正。 总时间复杂度 $O(n+m\log n)$。 const int MAXN=3e5+5; struct Edge{ int to,next; }edge[MAXN<<1]; int head[MAXN],edge_cnt; void Insert(int u,int v){ edge[++edge_cnt]=Edge{v,head[u]}; head[u]=edge_cnt; } namespace LCA{ int d[MAXN],sz[MAXN],f[MAXN]; int h_son[MAXN],mson[MAXN],p[MAXN]; void dfs_1(int u,int fa,int depth){ sz[u]=1;f[u]=fa;d[u]=depth;mson[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs_1(v,u,depth+1); sz[u]+=sz[v]; if(sz[v]>mson[u]) h_son[u]=v,mson[u]=sz[v]; } } void dfs_2(int u,int top){ p[u]=top; if(mson[u])dfs_2(h_son[u],top); for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==f[u]||v==h_son[u]) continue; dfs_2(v,v); } } void Init(int root){dfs_1(root,0,0);dfs_2(root,root);} int query_lca(int u,int v){ while(p[u]!=p[v]){ if(d[p[u]] ===== I. K 小数查询 ===== ==== 题意 ==== 给定一个长度为 $n$ 的序列,接下来 $q$ 个操作: - 选取区间 $[l,r]$,$a_i=\min(a_i,v)(l\le i\le r)$ - 询问区间 $[l,r]$ 的第 $k$ 小元素 ==== 题解 ==== 建立线段树套权值线段树。对于查询操作,考虑二分答案,然后查询区间中小于二分值的数个数判定答案合法性,时间复杂度 $O(\log^3 n)$。 对于修改操作,考虑在线段树中找到对应区间,然后暴力修改该区间及其所有祖先结点对应的权值线段树。 修改完成后同样打上懒标记,必要时下放标记。 显然每次修改复杂度为 $O(k\log^2 n)$,其中 $k$ 为该次修改删除的权值线段树的叶子数。 由于初始时仅有 $O(n\log n)$ 个权值线段树的叶子结点,于是修改操作的总时间复杂度为 $O(n\log^3 n)$。 于是总时间复杂度 $O(n\log^3 n)$,总空间复杂度 $O(n\log^2 n)$。 const int MAXN=8e4+5,MAXM=400,Inf=1e9; int a[MAXN],root[MAXN<<2],lef[MAXN<<2],rig[MAXN<<2],lazy[MAXN<<2]; int n,sz,s[MAXN*MAXM],ch[MAXN*MAXM][2]; void update_1D(int &k,int pos,int v,int lef=1,int rig=n){ if(!k)k=++sz; s[k]+=v; if(lef==rig)return; int mid=lef+rig>>1; if(pos<=mid) update_1D(ch[k][0],pos,v,lef,mid); else update_1D(ch[k][1],pos,v,mid+1,rig); } void build(int k,int L,int R){ lef[k]=L,rig[k]=R,lazy[k]=Inf; _rep(i,L,R)update_1D(root[k],a[i],1); if(L==R)return; int M=L+R>>1; build(k<<1,L,M); build(k<<1|1,M+1,R); } vector >del; void dfs(int k,int pos,int lef=1,int rig=n){ if(!s[k])return; if(lef==rig){ del.push_back(make_pair(lef,s[k])); return; } int mid=lef+rig>>1; if(pos>1; if(pos>=1; } return; } push_down(k); int mid=lef[k]+rig[k]>>1; if(mid>=L)update_2D(k<<1,L,R,v); if(mid>1; if(mid>=R)return query_1D(ch[k][0],L,R,lef,mid); else if(mid>1; if(mid>=R)return query_2D(k<<1,L,R,v); else if(mid=n)continue; update_2D(1,l,r,v); } else{ int lef=1,rig=n,mid,ans=-1; while(lef<=rig){ mid=lef+rig>>1; if(query_2D(1,l,r,mid)>=v){ ans=mid; rig=mid-1; } else lef=mid+1; } enter(ans); } } return 0; }