1. [ABC313G] Redistribution of Piles
简要题意
有 \(n\) 个盘子和 \(1\) 个袋子。初始时,第 \(i\) 个盘子上有 \(a_i\) 块石头,袋子是空的。
有以下两种操作:
- 对于每个盘子,如果当前盘子上有石头,那么从该盘子上移除一块石头。然后将所有移除掉的石头放进袋子。
- 从袋子里拿出 \(n\) 块石头,并依次在每个盘子中放入 \(1\) 块。这种操作能够进行当且仅当此时袋子里有至少 \(n\) 块石头。
你可以按任意顺序进行任意多次操作,包括 \(0\) 次。求所有能够到达的局面数量,对 \(998244353\) 取模。
\(1 \leq n \leq 2 \times 10^5\),\(0 \leq a_i \leq 10^9\)。
我们称一种局面为一种“状态”当且仅当这种局面中存在某个盘子为空。称一种局面与一种状态互相对应当且仅当从该状态出发,能够只经过操作 2 变成该局面。显然,一种局面对应的状态是唯一的(只需不断地从每个盘子中移除一块石头,直到某个盘子为空),但一种状态可能对应多个局面。因此答案即为所有状态对应的局面数量之和。
考虑如何求出一种状态对应的局面数量。显然每次操作会多出恰好 \(n\) 块石头和恰好一种局面。因此,如果总共有 \(\mathrm{sum}\) 块石头,且该状态有 \(x\) 块石头,则该状态会对应 \(\lfloor \dfrac{sum - x}{n} \rfloor + 1 = \lfloor \dfrac{sum - x + n}{n} \rfloor\) 种状态。
然而我们发现这道题的值域(单个盘子上的石子数)是 \(10^9\) 级别的,这说明虽然我们刚才的操作大大优化了复杂度,但现在仅连状态数都仍然不可接受。我们考虑将这些状态归类,定义一个状态的类别为这个状态中空盘子的数量,并设计一种算法快速地求出每一类包含的局面数量。事实上,若初始时将这些盘子按石子数升序排序,则每种状态中的空盘子一定集中在最左边,这种状态的类别即为最右端空盘子的编号。由此,对于一种类别,我们就能快速表示出它对应的状态。
现在我们只需考虑如何求出第 \(i\) 类状态包含的局面数。事实上,我们发现状态之间只能通过操作 1 转移。因此,如果我们找出了石子数最大的状态,该类别中的状态都可以被表示成一个非负整数 \(k\),表示保持左边的 \(i\) 个空盘子不变,右边 \(n - i\) 个非空的盘子上石子数量都减少 \(k\)。显然 \(k\) 的值域为 \([0, a_i - a_{i + 1})\),且其中的每个整数都会出现恰好 \(1\) 次。将上面的式子代入,我们得出第 \(i\) 类状态包含的局面数为
其中 \(\mathrm{sum}\) 表示总石子数,\(x\) 表示该类中石子数最大的状态中的石子数。
我们发现这个式子满足 floor-sum 算法的要求,因此只需调用 \(f(n - i, \mathrm{sum} - x + n, n, a_{i + 1} - a_i)\) 即可。时间复杂度为 \(\Theta(n \cdot \log V)\)。
AC 代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N=200003,mod=998244353;
int n,a[N];
long long floor(long long a,long long b){return (a>=0)?(a/b):-((-a+b-1)/b);
}
int f(long long a,long long b,int c,int n){// calculate the sum of floor(a * i + b)/c (1 <= i <= n).int sa=(floor(a,c)%mod+mod)%mod,sb=(floor(b,c)%mod+mod)%mod;int ans=(((long long)(n+1)*n/2%mod*sa%mod+(long long)n*sb%mod)%mod+mod)%mod;a-=floor(a,c)*c,b-=floor(b,c)*c;if(n==0||a==0) return ans;return ((a*n+b)/c*n%mod-f(c,-b-1,a,(a*n+b)/c)+ans+mod)%mod;
}
int main(){
// freopen("pile.in","r",stdin);
// freopen("pile.out","w",stdout);int i,ans=0;long long sum=0;scanf("%d",&n);for(i=1;i<=n;i++)scanf("%d",&a[i]);sort(a+1,a+n+1);for(i=1;i<=n;i++) sum+=a[i];for(i=n;i>0;i--) a[i]-=a[1];for(i=1;i<=n;i++) sum-=a[i];for(i=1,ans=(sum/n+1)%mod;i<n;i++){ans=(ans+f(n-i,sum+n,n,a[i+1]-a[i]))%mod;sum+=(long long)(a[i+1]-a[i])*(n-i);}printf("%d",ans);
// fclose(stdin);
// fclose(stdout);return 0;
}
2. [ABC366F] Maximum Composition
简要题意
给定 \(n\) 个函数 \(f_i\),其中 \(f_i(x) = a_i x + b_i\)。
对于所有元素互不相同的序列 \(p = (p_1, p_2, \cdots, p_k)\),其中所有元素均在 \([1, n]\) 中,求 \(f_{p_1}(f_{p_2}(\cdots(f_{p_k}(1))))\) 的最大值。
\(1 \leq n \leq 2 \times 10^5\),\(1 \leq k \leq \min\{ n, 10 \}\),\(1 \leq a_i, b_i \leq 50\)。
首先有一个很显然的性质,就是这个复合函数是单调递增的。因此若 \(p\) 的前面确定,则只需最大化后面的函数值。考虑对于给定的一组 \(p_k\),将它们如何排列才能使得答案最大。
假设我们已经找到了一组最优解 \(p\)。考虑对于 \(p\) 中相邻的两项 \(i\) 和 \(j\),其中 \(i\) 在 \(j\) 前面,交换它们的顺序对函数值的大小有什么影响。由上面的分析,对整体函数值的影响等价于对以 \(i\) 开头的后缀的函数值的影响。而又由假设,\(j\) 之后的函数值是确定的,因此只需比较以下两个函数的大小:
把它们的表达式写出来,得
由最优解的假设,必有 \(f_i(f_j(x)) \geq f_j(f_i(x))\)。这等价于
移项,得
移项,得
因此,对变量 \(i\),最优解 \(p\) 一定满足 \(\dfrac{a_i - 1}{b_i}\) 的值单调不增。即只需将给定的数组 \(\{(a_i, b_i)\}\) 按 \(\dfrac{a_i - 1}{b_i}\) 从大到小排序,然后 01 背包即可。
时间复杂度为 \(\Theta(nk)\)。
AC 代码
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N=200003,M=13;
const long long NIN=-1e17;
int n,m,a[N],b[N],c[N];
long long f[N][M];
bool cmp(const int& u,const int& v){return (a[u]-1)*b[v]<(a[v]-1)*b[u];
}
int main(){
// freopen("composition.in","r",stdin);
// freopen("composition.out","w",stdout);int i,j;scanf("%d%d",&n,&m);for(i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]);for(i=1;i<=n;i++) c[i]=i;sort(c+1,c+n+1,cmp);memset(f,NIN,sizeof f);f[0][0]=1;for(i=1;i<=n;i++)for(j=1,f[i][0]=1;j<=m;j++)f[i][j]=max(f[i-1][j],f[i-1][j-1]*a[c[i]]+b[c[i]]);printf("%lld",f[n][m]);
// fclose(stdin);
// fclose(stdout);return 0;
}
3. [ABC371G] Lexicographically Smallest Permutation
简要题意
给定序列 \((1, 2, \cdots, n)\) 的两个排列 \(A = (A_1, A_2, \cdots, A_n)\) 和 \(C = (C_1, C_2, \cdots, C_n)\)。
定义一次操作为对所有 \(i = 1, 2, \cdots, n\),将 \(C_i\) 同时替换为 \(C_{A_i}\)。
你可以进行任意多次上述操作,包括 \(0\) 次。求所有能够得到的 \(C\) 序列中字典序最小的序列。
\(1 \leq n \leq 2 \times 10^5\),\(1 \leq A_i, C_i \leq n\)。
考虑建出一个 \(n\) 个点、\(n\) 条边的有向图,对于每个 \(A_i\),在图中连边 \(i \rightarrow A_i\)。容易发现图中每个点的入度、出度均为 \(1\),因此这个图是由若干个并列的简单环组成的。
这么连边的意义在于我们可以很方便地找到任意次操作后序列 \(C\) 会变成什么。对于任意一个点 \(u\),设从 \(u\) 出发顺着每个点唯一的出边走 \(k\) 步后到达点 \(v\),那么经过 \(k\) 次操作后 \(C_u\) 的值即为最开始的 \(C_v\)。由此,我们称点 \(i\) 的权值为 \(C_i\)。现在问题转化成:
对于一个非负整数 \(k\),由 \(k\) 生成一个序列,序列的第 \(i\) 项为图中从点 \(i\) 出发走 \(k\) 步走到的点的点权。你需要最小化该序列的字典序。
接下来我们考虑怎么求出答案。由字典序的性质,我们需要首先保证点 \(1\) 走到的点权值最小,再保证点 \(2\) 走到的点权值最小,再保证点 \(3\) 走到的点权值最小,以此类推。因此,我们按这个顺序贪心地选取答案每一项。我们考虑现在枚举到点 \(i\),序列前 \(i - 1\) 项的答案已经确定。我们找出 \(i\) 所在的环,设这个环上编号最小的点为 \(i'\)。显然有 \(i' \leq i\)。若 \(i' < i\),确定了点 \(i'\) 走到的点,相当于整个环上所有点的答案已经确定了,因此我们无需再求一遍点 \(i\) 走到的点。
我们现在考虑 \(i' = i\) 的情况。一个容易想到的贪心是将 \(i\) 走到环上权值最小的点,但这样可能会和前面的答案矛盾。具体来说,假设有两个环,环上点的编号按边的方向依次为 \(1, 2\) 和 \(3, 4, 5, 6\),其中 \(1\) 和 \(4\) 的权值分别最小,那么在第一个环上你需要从 \(1\) 开始走偶数步才能走到答案,第二个环上你需要从 \(3\) 开始走奇数步才能走到答案(严格来说步数模 \(4\) 余 \(1\)),两个环上的答案不可能同时得到。我们由此受到启发,得到了判断答案间是否出现矛盾的方法:
设当前确定了 \(m\) 个环的答案,第 \(j\) 个环的长度为 \(p_j\),每个点出发走 \(a_j\) 步第一次走到答案。那么这组答案合法当且仅当存在非负整数 \(k\) 使得对所有 \(j\),均有 \(k \equiv a_j \pmod {p_j}\)。
我们只需枚举环上的点作为 \(i\) 可能的答案,再在所有合法的可能值中找出点权最小的。但问题在于 \(k\) 的大小没有限制,直接用上述方法判断显然难以接受。为了减小判断合法性的时间复杂度,我们想到将模数拆分。我们考虑中国剩余定理的一个推论:
对于正整数 \(x, y, t\),\(x \equiv y \pmod t\) 当且仅当对于所有满足 \(p\) 是质数且 \(p^a | t\) 的二元组 \((p, a)\),均有 \(x \equiv y \pmod {p^a}\)。
所有这样的 \((p, a)\) 可以通过质因数分解求得。我们考虑所有环带来的限制 \(k \equiv a_j \pmod {p_j}\)(也就是上面那个式子),我们将 \(p_j\) 转化为其能分解出的 \((p, a)\) 对应的限制 \(k \equiv a_j \pmod {p^a}\)。也就是说对于所有这样的模数 \(p^a\),我们都要求有一个唯一的余数。可以用一个数组 \(f(x)\) 求出我们要求 \(k \equiv f(x) \pmod x\)。我们判断一个新加入的答案是否合法,只需考虑其对应的所有 \((p, a)\) 是否满足这个式子即可。
时间复杂度为 \(\Theta(n \sqrt n)\)。
AC 代码
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
const int N=200003;
bool bj[N];
int n,p,a[N],c[N],f[N],ans[N];
vector<int> b[N];
int main(){
// freopen("permutation.in","r",stdin);
// freopen("permutation.out","w",stdout);int i,j,k,l,s1,s2,x; bool can;scanf("%d",&n);for(i=1;i<=n;i++)scanf("%d",&a[i]);for(i=1;i<=n;i++)scanf("%d",&c[i]);for(i=1;i<=n;i++){if(bj[i]) continue;b[++p].push_back(i),bj[i]=1;for(j=a[i];j!=i;j=a[j])b[p].push_back(j),bj[j]=1;}memset(f,-1,sizeof f);for(i=1;i<=p;i++){s1=n+1,s2=-1;for(j=0;j<b[i].size();j++){for(k=2,x=b[i].size(),can=1;k*k<=x;k++){if(x%k>0) continue;for(l=k;x%k==0;l*=k,x/=k)if(f[l]>=0&&j%l!=f[l]) can=0;}if(x>1&&f[x]>=0&&j%x!=f[x]) can=0;if(can&&c[b[i][j]]<s1)s1=c[b[i][j]],s2=j;}for(j=0;j<b[i].size();j++)ans[b[i][j]]=b[i][(j+s2)%b[i].size()];for(j=2,x=b[i].size();j*j<=x;j++){if(x%j>0) continue;for(k=j;x%j==0;k*=j,x/=j)f[k]=s2%k;}if(x>1) f[x]=s2%x;}for(i=1;i<=n;i++)printf("%d ",c[ans[i]]);
// fclose(stdin);
// fclose(stdout);return 0;
}
4. [ABC372G] Ax + By < C
简要题意
给定三个长为 \(n\) 的序列 \(A = (A_1, A_2, \cdots, A_n), B = (B_1, B_2, \cdots, B_n), C = (C_1, C_2, \cdots, C_n)\)。你需要求出满足以下条件的二元组 \((x, y)\) 的数量:
- \(x, y\) 都是正整数。
- 对每个 \(1 \leq i \leq n\),都有 \(A_i x + B_i y < C_i\)。
单个测试点内有 \(T\) 组测试数据,你需要对每组数据都求出答案。可以证明这样的二元组数量一定是有限的。
\(1 \leq T, n, \sum n \leq 2 \times 10^5\),\(1 \leq A_i, B_i, C_i \leq 10^9\)。
对于限制 \(A_i x + B_i y < C_i\),变形得 \(y \leq -\dfrac{A_i}{B_i} x + \dfrac{C_i - 1}{B_i}\),我们发现这类似于直线的解析式。考虑把这些条件画到平面直角坐标系上,那么一个条件相当于钦定第一象限上的点 \((x, y)\) 必须在某条直线上或在这条直线的下方。也就是说,每个条件都限制了 \((x, y)\) 必须在某个半平面上。为了综合这些限制,我们求一遍半平面交,相当于求出了这些直线围成的下凸壳(再加上两条坐标轴)。
显然对于直线 \(x = x_0\),若该直线与凸壳交点的纵坐标为 \(y_0\),那么横坐标为 \(x_0\) 的点就有 \(\lfloor y_0 \rfloor\) 个。我们考虑分别对于凸壳上的每一段求出其下方的点对数量,令第 \(i\) 段为 \(y = -\dfrac{a_i}{b_i} x + \dfrac{c_i - 1}{b_i}(l_i < x \leq r_i)\),那么将两个公式结合起来即可得到答案为
我们发现这个式子满足 floor-sum 算法的要求,因此只需调用 \(f(-a_i, l_i k + c_i - 1, b_i, r_i - l_i)\) 即可。时间复杂度为 \(\Theta(\sum n \cdot \log V)\)。
AC 代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N=200003;
const long long PIN=4557430888798830399;
struct Line{int a,b,c; // y = -(a / b) * x + (c / b)
}a[N],b[N];
const Line era={0,1,0};
int n,m; long long c[N];
bool cmp(const Line& l1,const Line& l2){return (long long)l1.a*l2.b<(long long)l1.b*l2.a;
}
long long sec(Line l1,Line l2){// It is guaranteed that a1 / b1 <= a2 / b2.if((long long)l1.c*l2.b> (long long)l1.b*l2.c) return -1;if((long long)l1.a*l2.b==(long long)l1.b*l2.a) return PIN;long long sa=(long long)l1.b*l2.a-(long long)l1.a*l2.b;long long sb=(long long)l1.b*l2.c-(long long)l1.c*l2.b;return sb/sa;
}
int floor(int a,int b){return (a>=0)?(a/b):-((-a+b-1)/b);
}
long long f(int a,int b,int c,int n){// Calculate the sum of (a * i + b) / c(1 <= i <= n).long long sa=floor(a,c),sb=floor(b,c);long long ans=sa*n*(n+1)/2+sb*n;a-=sa*c,b-=sb*c; if(a==0||n==0) return ans;return ((long long)a*n+b)/c*n-f(c,-b-1,a,((long long)a*n+b)/c)+ans;
}
int main(){
// freopen("line.in","r",stdin);
// freopen("line.out","w",stdout);int i,t; long long ans;for(scanf("%d",&t);t>0;t--){scanf("%d",&n),m=0;for(i=1;i<=n;i++)scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c),a[i].c--;sort(a+1,a+n+1,cmp);for(i=1;i<=n;i++){while(m>0&&sec(b[m],a[i])==-1) m--;while(m>1&&sec(b[m],a[i])<=sec(b[m-1],b[m])) m--;b[++m]=a[i];}while(m>1&&sec(era,b[m-1])<=sec(b[m-1],b[m])) m--;for(i=1;i<m;i++)c[i]=sec(b[i],b[i+1]);c[m]=sec(era,b[m]),ans=0;for(i=1;i<=m;i++){ans+=f(-b[i].a,b[i].c,b[i].b,c[i ]);ans-=f(-b[i].a,b[i].c,b[i].b,c[i-1]);}printf("%lld\n",ans);}
// fclose(stdin);
// fclose(stdout);return 0;
}
5. [ABC373G] No Cross Matching
简要题意
给定两个长为 \(n\) 的点集 \(P = \{ P_1, P_2, \cdots, P_n \}, Q = \{ Q_1, Q_2, \cdots, Q_n \}\),保证任意三点不共线。以这些点为基础建立一个平面直角坐标系,则每个点的坐标都是已知的。
现在我们在这些点间连 \(n\) 条无向边(即线段),每条边从一个 \(P\) 中的点连向一个 \(Q\) 中的点。称一种方案为一个“匹配”当且仅当图中每个点的度数均为 \(1\)。定义一个匹配“合法”当且仅当我们连出的任意两条线段都不想交。
你需要构造一个合法的匹配,或报告无解。
\(1 \leq n \leq 300\),\(1 \leq a_i \leq 5000\)。
我们定义一个匹配的权值为其中所有线段的长度之和。那么我们断言:权值最小的匹配一定合法。因为对于一个不合法的匹配,考虑两条相交的线段(不妨设为 \(P_1Q_1\) 和 \(P_2Q_2\),记它们交于点 \(O\));根据三角形的三边关系,必有 \(OP_1 + OQ_2 > P_1Q_2, OQ_1 + OP_2 > P_2Q_1\),将两式的三项对应相加得 \(P_1Q_1 + P_2Q_2 > P_1Q_2 + P_2Q_1\);因此,如果将这两条线段改为 \(P_1Q_2\) 和 \(P_2Q_1\),显然这仍然是一个匹配且线段总长度更小。所以,不合法的匹配一定不是权值最小的,因此权值最小的匹配一定合法。
问题转化为如何找出权值最小的匹配,我们发现这可以用费用流解决。具体地,从原点向每个 \(P_i\) 连容量为 \(1\)、费用为 \(0\) 的边;从每个 \(Q_j\) 向汇点连容量为 \(1\)、费用为 \(0\) 的边;从每个 \(P_i\) 向每个 \(Q_j\) 连容量为 \(1\)、费用为线段 \(P_iQ_j\) 的长度的边(费用可能为浮点数)。显然有解当且仅当该图的最大流为 \(n\);构造方案只需考虑跑完网络流后所有 \(P_i\) 连向 \(Q_j\) 的边,若流量为 \(1\) 则说明在匹配中。
因此我们只需建图后跑一遍最小费用最大流。然而常用的 SSP 算法复杂度为 \(\Theta(fnm) = \Theta(n^4)\),无法接受。你需要使用复杂度更为优秀的 Primal-Dual 算法,在 \(\Theta(nm + fn^2) = \Theta(n^3)\) 的时间复杂度内通过此题。
AC 代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <utility>
#define x first
#define y second
using namespace std;
const int P=303,N=P*2,M=P*P+N;
const int PIN=1061109567;
int n,to[P][P];
int len_list=0,e[M*2],ne[M*2],h[N];
pair<int,double> w[M*2];
pair<int,int> a[P],b[P];
double get_dist(pair<int,int> u,pair<int,int> v){return sqrt((u.x-v.x)*(u.x-v.x)+(u.y-v.y)*(u.y-v.y));
}
int dotl(int u){ return u ; }
int dotr(int u){ return u+n; }
void add(int a,int b,pair<int,double> val){e[len_list]=b;w[len_list]=val;ne[len_list]=h[a];h[a]=len_list++;
}
void add_once(int a,int b,pair<int,double> val){add(a,b,val);add(b,a,make_pair(0,-val.y));
}
void Bellman_Ford(int S,double pot[N]){int i,j; double ls[N];for(i=0;i<=n*2+1;i++)pot[i]=PIN;pot[S]=0;for(i=1;i<=n*2+1;i++){for(j=0;j<=n*2+1;j++) ls[j]=pot[j];for(j=0;j<len_list;j+=2)if(w[j].x>0&&ls[e[j^1]]+w[j].y<pot[e[j]])pot[e[j]]=ls[e[j^1]]+w[j].y;}
}
bool Dijkstra(int S,int T,double pot[N],int pre[N],int la[N],double& val){bool bj[N]={0}; int i,j,s2;double s1,dis[N];for(i=0;i<=n*2+1;i++)dis[i]=PIN,pre[i]=la[i]=-1;for(i=0,dis[S]=0;i<=n*2+1;i++){s1=PIN,s2=-1;for(j=0;j<=n*2+1;j++)if(!bj[j]&&dis[j]<s1)s1=dis[j],s2=j;if(s2==-1) break;for(j=h[s2],bj[s2]=1;j>=0;j=ne[j])if(w[j].x>0&&dis[s2]+w[j].y+pot[s2]-pot[e[j]]<dis[e[j]]){dis[e[j]]=dis[s2]+w[j].y+pot[s2]-pot[e[j]];pre[e[j]]=s2,la[e[j]]=j;}}val=dis[T]-pot[S]+pot[T];for(i=0;i<=n*2+1;i++) pot[i]+=dis[i];return dis[T]<PIN/2;
}
pair<int,double> Primal_Dual(int S,int T){double pot[N];pair<int,double> ans(0,0);Bellman_Ford(S,pot);while(true){int pre[N]={0},la[N]={0},i;pair<int,double> s1(PIN,0);if(!Dijkstra(S,T,pot,pre,la,s1.y)) break;for(i=T;i!=S;i=pre[i])s1.x=min(s1.x,w[la[i]].x);s1.y*=s1.x,ans.x+=s1.x,ans.y+=s1.y;for(i=T;i!=S;i=pre[i])w[la[i]].x-=s1.x,w[la[i]^1].x+=s1.x;}return ans;
}
int main(){
// freopen("matching.in","r",stdin);
// freopen("matching.out","w",stdout);int i,j;scanf("%d",&n);for(i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y);for(i=1;i<=n;i++)scanf("%d%d",&b[i].x,&b[i].y);memset(h,-1,sizeof h);for(i=1;i<=n;i++)for(j=1;j<=n;j++){to[i][j]=len_list;add_once(dotl(i),dotr(j),make_pair(1,get_dist(a[i],b[j])));}for(i=1;i<=n;i++)add_once(0,dotl(i),make_pair(1,0));for(i=1;i<=n;i++)add_once(dotr(i),n*2+1,make_pair(1,0));if(Primal_Dual(0,n*2+1).x<n)printf("-1");elsefor(i=1;i<=n;i++)for(j=1;j<=n;j++)if(w[to[i][j]^1].x==1)printf("%d ",j);
// fclose(stdin);
// fclose(stdout);return 0;
}
6. [ABC376G] Treasure Hunting
简要题意
给定一棵 \(n + 1\) 个点的树,点编号从 \(0\) 到 \(n\)。根结点为 \(0\),点 \(i(1 \leq i \leq n)\) 的父亲为 \(p_i\),且有点权 \(a_i\)。
树上有一个非根结点藏了宝藏,但你不知道宝藏具体在哪个结点。你只知道宝藏在点 \(i\) 的概率为 \(\dfrac{a_i}{\sum_{k = 1}^n a_k}\)。
为了找到宝藏,你需要按顺序搜索树上的结点。初始时你在 \(0\) 号结点,标志着你已经搜索过根结点。你需要不断进行如下操作,直到搜到宝藏为止:
- 选择一个未搜索过的结点 \(u\),满足 \(u\) 的父亲 \(p_u\) 已经被搜索过;然后搜索结点 \(u\)。
你想尽快找到宝藏。因此你想知道,若你按照最优策略进行搜索,操作次数的期望值最小是多少。答案对 \(998244353\) 取模。
每个测试点内有 \(T\) 组测试数据。
\(1 \leq T, n, \sum n \leq 2 \times 10^5\),\(a_i \geq 1\),\(\sum a_i \leq 10^8\)。
发现最优策略一定形如:我们按照一个固定的顺序搜索整棵树。令这个顺序为 \(s_1, s_2, \cdots, s_n\),记 \(X = \sum_{k = 1}^n a_k\),由期望可加性可知答案为 \(X + (X - a_{s_1}) + (X - a_{s_1} - a_{s_2}) + \cdots + (X - a_{s_1} - a_{s_2} - \cdots - a_{s_n})\) 的值再除以 \(X\)。由于除数是个常数,我们不妨忽略它,下文的“答案”即指这里的被除数。
如果选择点 \(u\) 时,它的父亲选不选都无所谓,那么相当于我们可以任意钦定搜索结点的顺序。考虑将答案化简为 \(nX - (n - 1)a_{s_1} - (n - 2)a_{s_2} - \cdots - 1 \cdot a_{s_{n - 1}} - 0 \cdot a_{s_n}\)。由排序不等式,\(a_i\) 越大的结点应该越先搜索。因此我们得出结论,每一轮操作中,我们总是选择剩余结点中 \(a_i\) 最大的进行搜索。
然而这个顺序不能任意钦定,搜索必须从根往下依次进行,具体地,我们定义一个结点能够搜索当且仅当它的父亲已被搜索。考虑扩展上述结论,维护一个集合 \(S\) 表示当前能够搜索的结点,那么存在如下贪心策略:每一轮操作中,我们总是选择集合 \(S\) 中 \(a_i\) 最大的进行搜索。这个贪心显然是错误的,因为我们很容易构造出反例:如果 \(S\) 中某个结点 \(u\) 的点权很小,但是 \(u\) 的某个儿子 \(v\) 的点权接近无穷大,那么我们为了较早地选 \(v\) 显然应该优先选择点 \(u\)。由此我们得出这个贪心结论的适用范围:对于每一点 \(u(p_u > 0)\),都有 \(a_{p_u} \geq a_u\)(因为这种情况下,按贪心策略得出的顺序与任意钦定选择顺序得出的最优策略相同)。
那么如果存在 \(a_u > a_{p_u}\) 的情况怎么办?不妨设 \(u\) 为 \(p_u\) 的儿子中点权最大的,由于我们抛弃上述贪心策略而先选 \(p_u\) 的目的就是选择点 \(u\),因此搜索 \(p_u\) 后我们就应该立即搜索点 \(u\)。由此我们不妨将 \(p_u\) 和 \(u\) 合并(注意这里是有序的),合并后的结点 \(p_u - u\) 表示:在最优选择序列中 \(p_u\) 和 \(u\) 应该是连续的,且 \(p_u\) 在 \(u\) 之前被选择。由此,我们得出本题的正确思路:
定义广义结点 \(s_1 - s_2 - \cdots - s_k\) 为原树上结点 \(s_1, s_2, \cdots, s_k\) 合并形成的新结点,表示在最优选择序列中这些结点所在的下标是连续的,且其内部顺序为 \(s_1, s_2, \cdots, s_k\)。我们为每个广义结点分配一个编号,定义广义结点 \(u\) 表示编号为 \(u\) 的广义结点。
对于树上所有广义结点,我们约定“任意钦定选择顺序”必须符合广义结点的定义,即其包含的结点下标连续且内部有序。我们定义广义结点 \(u\) 比 \(v\) 优当且仅当若任意钦定选择顺序,其他结点顺序不变时,先选 \(u\) 中的结点得到的答案小于先选 \(v\) 中的结点得到的答案。
对于本题,如果存在一个广义结点 \(v\) 满足 \(v\) 优于它的父亲 \(u\),那么合并 \(u\) 和 \(v\)。不断合并结点直到所有的广义结点都劣于它的父亲。然后我们按如下贪心策略进行:维护集合 \(S\) 表示当前能够搜索的广义结点,每一轮操作中,我们总是选择 \(S\) 中最优的广义结点进行搜索。
剩下的问题为:如何判定两个广义结点谁优谁劣,以及按什么顺序合并结点。
对于两个广义结点 \(u\) 和 \(v\),我们考虑任意钦定选择顺序后的一种选择顺序,满足 \(u\) 在 \(v\) 前面且 \(u\) 和 \(v\)。记 \(u\) 为广义结点 \(s_1 - s_2 - \cdots - s_p\),\(v\) 为 \(t_1 - t_2 \cdots - t_q\),\(u\) 前面所有结点的点权和为 \(X - x\)。那么
- 按原顺序,先选 \(u\) 后选 \(v\) 的值为:
- 交换顺序,先选 \(v\) 后选 \(u\) 的值为:
其中 \(A\) 和 \(B\) 是在两式中不变的值。若要让前者小于后者,则有 \(q \sum a_{s} > p \sum a_{t}\),即 \(\dfrac{\sum a_{s}}{p} > \dfrac{\sum a_{t}}{q}\)。因此,我们定义广义结点 \(u\) 的权值为\(u\) 中结点的权值和除以 \(u\) 中结点的个数,那么广义结点 \(u\) 优于 \(v\) 当且仅当 \(u\) 的权值比 \(v\) 大。具体地,对于每个广义结点,我们只需维护三个值:其包含的结点个数,结点的权值和,结点权值前缀和的和。
接下来考虑合并结点的顺序。考虑维护集合 \(S\) 表示在之后的合并操作中,\(S\) 以外的点都不会作为儿子进行合并。每轮合并操作时,作为儿子的广义结点 \(u\) 需要满足如下条件:
- 若合并操作能够进行(\(u\) 优于父亲),则 \(u\) 一定是所有兄弟中权值最大的。
- 若合并操作不能进行(\(u\) 不优于父亲),为保证时间复杂度(可以直接在 \(S\) 中删去 \(u\)),此后不能存在任意时刻使得 \(u\) 优于父亲。
对于第一条限制,我们只需找出权值最大的点 \(u\) 进行合并即可。对于第二条限制,若 \(u\) 严格劣于父亲,则说明父亲不在 \(S\) 中,\(u\) 符合条件;若 \(u\) 和父亲的点权相等,为减小判断难度,我们应选择父亲进行合并而不是 \(u\)。因此我们按如下策略进行合并:每次找到 \(S\) 中权值最大的点进行合并。若有多个点符合条件,取 dfs 序最小的。
于是我们解决了这道题。实现时,合并过程和求答案过程中的集合 \(S\) 都可以用堆维护。对于合并两个结点的儿子的操作,使用启发式合并即可。总时间复杂度为 \(\Theta(\sum n \cdot \log n)\)。
AC 代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
const int N=200003,mod=998244353;
struct Node{int cnt,sum;long long tot;
}a[N];
bool ban[N];
int n,fa[N],dfn[N];
vector<int> e[N];
bool cmp(int u,int v){if((long long)a[u].sum*a[v].cnt!=(long long)a[v].sum*a[u].cnt)return (long long)a[u].sum*a[v].cnt>(long long)a[v].sum*a[u].cnt;return dfn[u]<dfn[v];
}
int pow(int a,int b){int ans=1;while(b>0){if(b&1) ans=(long long)ans*a%mod;a=(long long)a*a%mod; b>>=1;}return ans;
}
void dfs(int u,int& p){dfn[u]=++p;for(int i=0;i<e[u].size();i++)dfs(e[u][i],p);
}
namespace Heap{int n,a[N],p[N];void up(int u){while(u>1&&cmp(a[u],a[u/2])){swap(p[a[u]],p[a[u/2]]);swap(a[u],a[u/2]),u/=2;}}void down(int u){while(u*2<=n&&cmp(a[u*2],a[u])||u*2<n&&cmp(a[u*2+1],a[u]))if(u*2==n||cmp(a[u*2],a[u*2+1])){swap(p[a[u]],p[a[u*2]]);swap(a[u],a[u*2]),u*=2;}else{swap(p[a[u]],p[a[u*2+1]]);swap(a[u],a[u*2+1]),u=u*2+1;}}void push(int u){a[++n]=u; p[u]=n; up(n);}void pop(){if(!n) return ;p[a[n]]=1; p[a[1]]=0;a[1]=a[n--]; down(1);}void erase(int u){if(!p[u]) return ;p[a[n]]=p[u]; a[p[u]]=a[n--];up(p[u]),down(p[u]); p[u]=0;}bool empty(){ return !n; }int top(){ return a[1]; }
}
int merge(int u,int v){// It is guaranteed that "u" is the father of "v".if(e[u].size()>=e[v].size()){a[u].tot+=a[v].tot+(long long)a[u].sum*a[v].cnt;a[u].cnt+=a[v].cnt,a[u].sum+=a[v].sum;for(int i=0;i<e[v].size();i++)if(!ban[e[v][i]]) fa[e[v][i]]=u;for(int i=0;i<e[v].size();i++)if(!ban[e[v][i]]) e[u].push_back(e[v][i]);ban[v]=1,e[v].clear(); return u;}else{a[v].tot+=a[u].tot+(long long)a[u].sum*a[v].cnt;a[v].cnt+=a[u].cnt,a[v].sum+=a[u].sum;for(int i=0;i<e[u].size();i++)if(!ban[e[u][i]]&&e[u][i]!=v) fa[e[u][i]]=v;for(int i=0;i<e[u].size();i++)if(!ban[e[u][i]]&&e[u][i]!=v)e[v].push_back(e[u][i]);fa[v]=fa[u],e[fa[u]].push_back(v);ban[u]=1,e[u].clear(); return v;}
}
int main(){
// freopen("treasure.in","r",stdin);
// freopen("treasure.out","w",stdout);int i,t,sum,s1,cur; long long ans;for(scanf("%d",&t);t>0;t--){scanf("%d",&n);for(i=0;i<=n;i++)e[i].clear(),ban[i]=0;for(i=1;i<=n;i++){scanf("%d",&fa[i]);e[fa[i]].push_back(i);}a[0]={0,0,0};for(i=1,sum=0;i<=n;i++){scanf("%d",&a[i].sum);a[i].cnt=1,a[i].tot=0;sum+=a[i].sum;}dfs(0,s1=0);for(i=1;i<=n;i++) Heap::push(i);while(!Heap::empty()){s1=Heap::top(),Heap::pop();if(fa[s1]&&cmp(s1,fa[s1])){Heap::erase(fa[s1]);Heap::push(merge(fa[s1],s1));}}Heap::push(0),ans=0,cur=sum;while(!Heap::empty()){s1=Heap::top(),Heap::pop();ans+=(long long)cur*a[s1].cnt-a[s1].tot;for(cur-=a[s1].sum,i=0;i<e[s1].size();i++)if(!ban[e[s1][i]]) Heap::push(e[s1][i]);}printf("%lld\n",ans%mod*pow(sum,mod-2)%mod);}
// fclose(stdin);
// fclose(stdout);return 0;
}
7. [ABC386G] Many MST
简要题意
考虑一类 \(n\) 个点的无向完全图,其中每条边的权值都在 \([1, m]\) 中。显然这样的图有 \(m^{\frac{n(n - 1)}{2}}\) 张。
对连通图 \(G_0\),记 \(W(G_0)\) 表示图 \(G_0\) 的最小生成树上的边权和。你需要对上述所有图 \(G\) 求出 \(w(G)\) 之和,答案对 \(998244353\) 取模。
\(2 \leq n \leq 500\),\(1 \leq m \leq 500\)。
感觉前面的转化很难想,但想通了后面的 DP 就容易了。
在本文中,我们约定 \(C(G)\) 表示 \(G\) 的所有连通块的点集构成的集合,\(E(G)\) 表示 \(G\) 的边集,\(T(G)\) 表示 \(G\) 的最小生成树(森林);\(S\) 表示所有满足条件的 \(G\) 构成的集合,\(G_{P}\) 表示只保留 \(G\) 所有点和满足条件 \(P\) 的边构成的新图。那么显然有 \(T_{w \leq x}(G) = T(G_{w \leq x})\)。
套路地,我们考虑拆贡献,对一条最小生成树上的边 \(e\),将 \(w(e)\) 拆成 \(\sum_{x = 0}^{m - 1} [w(e) > x]\)。这样做的好处是我们可以将每条边的每一项和在一起算,于是问题就简化为求最小生成森林上的边数。具体地,有
接下来是本题最难的地方:将最小生成森林的边数转化为森林的连通块数。为什么要这么做呢?首先,一棵树的边数与连通块数有简单的关系;其次,对于一张图,其生成森林的连通块数与它本身的连通块数是相等的。这么转化后我们就不用再非常麻烦地求最小生成森林,而可以直接在原图上求解。具体地,有
于是问题转化为:定义 \(u, v\) 连通当且仅当从 \(u\) 出发只经过边权不超过 \(x\) 的边到达 \(v\),求所有图的连通块数量之和。然而直接 DP 还是不好做。考虑继续拆贡献,我们枚举每个点集 \(V\),判断它是否为图 \(G\) 的连通块。具体地,设图 \(G\) 的点集为 \(I = {1, 2, \cdots, n}\),有
这样做的好处就很明显了。由于图 \(G\) 是完全图,因此任意两个大小相等的点集 \(V\),它们的贡献是一样的。于是我们只需枚举点集的大小,即
答案即为:
我们要求图 \(G_{w \leq x}\) 恰有一个连通块为 \(1, 2, \cdots, i\),求图 \(G\) 的数量。事实上,我们只需求出由这 \(i\) 个点组成的连通分量的数量 \(t\),答案即为 \(t(m - x)^{i(n - i)} m^{\frac{(n - i)(n - i - 1)}{2}}\)(要求剩余点与前 \(i\) 个点之间的边权均大于 \(x\),剩余点之间边权随便)。我们发现求 \(t\) 的过程与下面的经典问题类似:
如何求出 $n$ 个点的简单连通图的数量?
考虑 DP,设 \(f(i)\) 表示 \(i\) 个点的简单连通图的数量。转移使用容斥原理进行,显然简单图的总数为 \(2^{\frac{n(n - 1)}{2}}\);为扣去不连通图的贡献,我们枚举 \(j\),判断有多少种简单图满足 \(1\) 所在的连通块点数为 \(j\)。显然这样的连通块有 \(\binom{i - 1}{j - 1}\) 种,此时我们额外要求:这 \(j\) 个点之间连通,与外界的点不连通。于是有转移
想必大家都已经会了,本题只需加上边权带来的影响即可。时间复杂度为 \(\Theta(mn^2)\)。
AC 代码
#include <cstdio>
const int N=503,mod=998244353;
int n,m,f[N],powm[N*N],powi[N*N],C[N][N];
int main(){
// freopen("MST.in","r",stdin);
// freopen("MST.out","w",stdout);int i,j,k,s1,ans=0;scanf("%d%d",&n,&m);for(i=1,powm[0]=1;i<=n*(n-1)/2+1;i++)powm[i]=(long long)powm[i-1]*m%mod;for(i=0;i<=n;i++)for(j=1,C[i][0]=C[i][i]=1;j<i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;for(i=0;i<m;i++){for(j=1,powi[0]=1;j<=n*(n-1)/2;j++)powi[j]=(long long)powi[j-1]*(m-i)%mod;for(j=1;j<=n;j++)for(k=1,f[j]=powm[j*(j-1)/2];k<j;k++){s1=(long long)f[k]*powi[k*(j-k)]%mod*powm[(j-k)*(j-k-1)/2]%mod;f[j]=(f[j]-(long long)s1*C[j-1][k-1]%mod+mod)%mod;}for(j=1;j<=n;j++){s1=(long long)f[j]*powi[j*(n-j)]%mod*powm[(n-j)*(n-j-1)/2]%mod;ans=(ans+(long long)s1*C[n][j]%mod)%mod;}}ans=(ans-powm[n*(n-1)/2+1]+mod)%mod;printf("%d",ans);
// fclose(stdin);
// fclose(stdout);return 0;
}