[复健]蓝书精刷 0x00 基本算法
0x01 位运算
快速幂
每一个正整数可以唯一表示为若干指数不重复的 \(2\) 的幂次的和.
设 \(b\) 在二进制表示下有 \(k\) 位,其中第 \(i(0 \leq i <k)\) 位的数字是 \(c_i\),那么:
于是:
因为 \(k= \lceil \log_2(b+1) \rceil\),所以上式乘积项不多于 \(\lceil \log_2(b+1) \rceil\) 项.
所以可以通过 \(k\) 次递推求出每个乘积项,当 \(c_i=1\) 时累计答案。复杂度为 \(O(\log b)\).
int qpow(int a,int b,int p) {
int res=1;
while(b) {
if(b&1) res=1ll*res*a%p;
a=1ll*a*a%p;
b>>=1;
}
return res;
}
变式1 龟速乘
基于二进制.
设 \(b\) 在二进制表示下有 \(k\) 位,其中第 \(i(0 \leq i <k)\) 位的数字是 \(c_i\),那么:
于是:
所以可以通过 \(k\) 次递推求出每个乘积项,复杂度 \(O(\log b)\).
ll times(ll a,ll b,ll p) {
ll res=0;
while(b) {
if(b&1) res=(res+a)%p;
a=a*2%p;
b>>=1;
}
return res;
}
状态压缩
一般数据范围很小的时候可以考虑.
状压 dp.
令 \(dp_{i,j}\) 表示当前路径状态为 \(i\),处于第 \(j\) 个节点时的最小贡献。
则状态转移方程为 \(dp_{i,j}=\min{dp_{i\ \text{xor}\ (1<<j),k}+w_{k,j}}\),其中 \(0 \leq k <n\) 且 \((i>>j)\&1=1,\ ((i\ \text{xor}\ (1<<j))>>k)\&1=1\).
复杂度 \(O(n^2 \times 2^n)\).
int dp[1<<N][N],w[N][N];
int hamilton() {
memset(dp,0x3f,sizeof(dp));
dp[1][0]=0;
for(int i=1;i<(1<<n);i++) {
for(int j=0;j<n;j++) {
if(!((i>>j)&1)) continue;
for(int k=0;k<n;k++) {
if(!(((i^(1<<j))>>k)&1)) continue;
dp[i][j]=std::min(dp[i][j],dp[i^(1<<j)][k]+w[k][j]);
}
}
}
return dp[(1<<n)-1][n-1];
}
位运算的一个性质:各个位之间是独立无关的.
该性质经常用于和位运算相关的贪心中.
本题从高到低贪心即可,当前为选 \(1\) 当且仅当值不超过 \(m\) 且答案比选 \(0\) 更优.
#include <cstdio>
const int N=1e5+10;
const int M=30;
int n,m;
char op[N][3];
int a[N];
int calc(int x,int i) {
for(int j=1;j<=n;j++) {
if(op[j][0]=='A') x&=((a[j]>>i)&1);
else if(op[j][0]=='O') x|=((a[j]>>i)&1);
else x^=((a[j]>>i)&1);
}
return x;
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%s%d",&op[i],&a[i]);
int val=0,ans=0;
for(int i=M-1;i>=0;i--) {
int res0=calc(0,i),res1=calc(1,i);
if(val+(1<<i)<m&&res1>res0) {
val+=(1<<i);ans+=(res1<<i);
} else ans+=(res0<<i);
}
printf("%d\n",ans);
return 0;
}
成对变换
对于非负整数 \(n\),当 \(n\) 为偶数时,\(n\ \text{xor}\ 1=n+1\),当 \(n\) 为奇数时,\(n\ \text{xor}\ 1=n-1\).
该性质常用于邻接表中.
lowbit 运算
\(\text{lowbit}(n)\) 定义为非负整数 \(n\) 在二进制表示下最低位的 \(1\) 及其后面所有的 \(0\) 构成的数值.
其公式为:
推导:
设 \(n>0,\ n\) 的第 \(k\) 位是最低位的 \(1\).
先把 \(n\) 取反,此时第 \(k\) 位变成 \(0\),第 \(0\sim k-1\) 位都变成 \(1\).
再 \(+1\),此时因为进位,第 \(k+1\) 位及更高位恰好与原来相反,第 \(k\) 位及更低位与原来相同.
在补码表示下,\(\sim n=-1-n\),因此得证.
配合 Hash 可以找出整数二进制表示下所有是 \(1\) 的位,只需不断令 \(n=n-\text{lowbit}(n)\) 即可,复杂度与 \(1\) 的个数同级.
同时 \(\log_2(\text{lowbit}(n))\) 即为 \(1\) 的位.
Trick:预处理 \(\log_2(n)\) 可以建立一个长度为 \(37\) 的数组 \(H\),令 \(H_{2^k \bmod 37}=k\).
原理是因为 \(\forall k \in [0,35],\ 2^k \bmod 37\) 互不相等,且恰好取遍整数 \(1 \sim 36\).
同时也是树状数组中的一个基本运算.
0x02 递推与递归
递推与递归
以下几种情况常用递归实现朴素算法:
- 多项式:状态空间规模为 \(n^c\),也可以用循环实现;
- 指数:状态空间规模为 \(c^n\),也常用位运算配合;
- 排列:状态空间规模为 \(n!\),也常与枚举排列配合;
- 组合:状态空间规模为 \(C_n^m\),常用剪枝降低复杂度.
一道指数型的枚举.
注意到三个性质:
- 每个位置至多会被点击 \(1\) 次;
- 若固定了第一行,则满足题意的点击方案至多只能有 \(1\) 种;
- 点击的先后顺序不影响最终结果.
因此,不妨先考虑第一行如何点击,在枚举第一行的点击方法后,就可以认为第一行固定不动,再自上往下考虑每个点是否需要点击。最后再检验即可.单次时间复杂度 \(O(2^n \times n^2)\).
int calc(int x[][6],int cnt) {
for(int i=1;i<5;i++) {
for(int j=0;j<5;j++) {
if(cnt>6) return 7;
if(!x[i-1][j]) {
++cnt;x[i-1][j]^=1;
x[i][j]^=1;x[i][j-1]^=1;
x[i+1][j]^=1;x[i][j+1]^=1;
}
}
}
for(int j=0;j<5;j++) if(!x[4][j]) return 7;
return cnt;
}
int c[6][6];
int d[6][6];
void sol() {
for(int i=0;i<5;i++) for(int j=0;j<5;j++) scanf("%1d",&c[i][j]);
int ans=7;
for(int s=0;s<32;s++) {
int cnt=0;
for(int i=0;i<5;i++) for(int j=0;j<5;j++) d[i][j]=c[i][j];
for(int i=0;i<5;i++) {
if((s>>i)&1) {
++cnt;
d[0][i]^=1;d[0][i-1]^=1;
d[1][i]^=1;d[0][i+1]^=1;
}
}
ans=std::min(ans,calc(d,cnt));
}
if(ans>6) puts("-1");
else printf("%d\n",ans);
}
语法点:多维数组传参时第二维起需确定大小.
先考虑经典的三个塔的汉诺塔问题.
设 \(d_n\) 表示 \(n\) 个盘子的最小步数,可以发现 \(d_n\) 同时表示将 \(n\) 个盘子移动到另一个塔上的最小步数.
因此可以先将 \(n-1\) 个小盘移动到非目标柱上,再将最大盘移动到目标柱,最后将 \(n-1\) 个小盘移动到目标柱.
则有 \(d_n=2d_{n-1}+1\),显然 \(d_1=1\),观察可得通项公式为 \(d_n=2^n-1\).
接下来考虑四个塔的问题.
设 \(f_n\) 表示 \(n\) 个盘子的最小步数.
可以先考虑将 \(j(1 \leq j<n)\) 个小盘移动到一根非目标柱上,然后剩余的 \(n-j\) 个小盘就化归为了三个塔的问题,而 \(j\) 个小盘调用前面答案即可.
即 \(f_n=\min(2f_i+d_{n-i})(1 \leq i <n)\),显然 \(f_1=1\).
int f[N],d[N];
memset(f,0x3f,sizeof(f));
d[1]=1;f[1]=1;
for(int i=2;i<=12;i++) d[i]=2*d[i-1]+1;
for(int i=2;i<=12;i++) for(int j=1;j<i;j++) f[i]=std::min(f[i],2*f[j]+d[i-j]);
分治
把 \(a\) 质因数分解表示为 \(p_1^{c_1} \times p_2^{c_2} \times \cdots \times p_n^{c_n}\).
那么 \(a^b\) 表示为 \(p_1^{bc_1} \times p_2^{bc_2} \times \cdots \times p_n^{bc_n}\).
\(a^b\) 的所有约数表示为集合 \(\{p_1^{k_1} \times p_2^{k_2} \times \cdots \times p_n^{k_n} \}\),其中 \(0 \leq k_i \leq bc_i(1 \leq i \leq n)\).
根据乘法分配律,所有约数之和就是:
观察到每个括号内的多项式为模意义下的等比数列求和,考虑用分治解决.
设 \(\text{sum}(p,c)=1+p+p^2+\cdots+p^c=?\).
若 \(c\) 为奇数,此时恰好可以分为两个等大的区间:
若 \(c\) 为偶数,此时为奇数项,取走最高项即可化归为 \(c\) 为奇数的情况,即:
配合快速幂,时间复杂度为 \(O(\log c)\).
#include <cstdio>
typedef long long ll;
const int mod=9901;
ll qpow(ll a,ll b) {
ll res=1;
while(b) {
if(b&1) res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
ll calc(ll p,ll c) {
if(c==0) return 1;
if(c&1) return (1+qpow(p,(c+1)/2))*calc(p,(c-1)/2)%mod;
else return ((1+qpow(p,c/2))*calc(p,c/2-1)%mod+qpow(p,c))%mod;
}
ll a,b,ans=1;
int main() {
scanf("%lld%lld",&a,&b);
for(int i=2;i*i<=a;i++) {
if(!(a%i)) {
ll c=0;
while(!(a%i)) {++c;a/=i;}
ans=(ans*calc(i,b*c))%mod;
}
}
if(a>1) ans=(ans*calc(a,b))%mod;
printf("%lld\n",ans);
return 0;
}
分形
分形问题常用分治解决。
为方便计算,我们从 \(0\) 开始编号.
本题关键在于求编号为 \(m\) 的房屋在 \(n\) 级城市中的位置,可以把该问题记为 \(\text{calc(n,m)}\).
求解分形问题时注意探究子形与母形的关系,即 \(n\) 级城市与 \(n-1\) 级城市之间的关系.
求解 \(\text{calc(n,m)}\) 时,因为 \(n-1\) 级城市有 \(2^{2n-2}\) 座房屋,所以我们先递归求解 \(\text{calc}(n-1,m \bmod 2^{2n-2})\),记为 \((x,y)\).
根据 \(\frac{m}{2^{2n-2}}\) 可以确定编号 \(m\) 的房屋处于四座 \(n-1\) 级城市中的哪一座.
- 若处于右上的 \(n-1\) 级城市中,则该房屋在 \(n\) 级城市中的坐标为 \((x,y+2^{n-1})\).
- 若处于右下的 \(n-1\) 级城市中,则该房屋在 \(n\) 级城市中的坐标为 \((x+2^{n-1},y+2^{n-1})\).
- 若处于左上的 \(n-1\) 级城市中,则先把该城市顺时针旋转 \(90\) 度,坐标为 \((y,-x)\),再水平翻转(关于纵轴对称),变成 \((y,x)\),则该房屋在 \(n\) 级城市中的坐标为 \((y,x)\).
- 若处于左下的 \(n-1\) 级城市中,则先把该城市逆时针旋转 \(90\) 度,坐标为 \((-y,x)\),再水平翻转变为 \((-y,-x)\),即在 \(n\) 级城市中坐标为 \((2^n-y-1,2^{n-1}-x-1)\).
关于坐标旋转:
坐标可以看作一个 \(2 \times 1\) 的矩阵 \(\begin{bmatrix} x \\ y \end{bmatrix}\),左乘旋转矩阵 \(\begin{bmatrix} \cos \theta & \sin \theta \\ -\sin \theta & \cos \theta \end{bmatrix}\) 即为原坐标顺时针旋转 \(\theta\) 角的结果.
可以用复数/几何推导.
std::pair<ll,ll> calc(int n,ll m) {
if(!n) return std::make_pair(0,0);
ll len=1ll<<(n-1),mod=1ll<<(2*n-2);
std::pair<ll,ll> pos=calc(n-1,m%mod);
ll x=pos.first,y=pos.second;
int id=m/mod;
if(!id) return std::make_pair(y,x);
if(id==1) return std::make_pair(x,y+len);
if(id==2) return std::make_pair(x+len,y+len);
if(id==3) return std::make_pair(2*len-y-1,len-x-1);
}
0x03 前缀和和差分
前缀和
蕴含着容斥原理的思想.
二维前缀和模板,注意细节处理:
- 注意题目从 \(0\) 开始编号;
- 注意越界问题.
#include <cstdio>
#include <algorithm>
const int N=5e3+10;
const int M=5e3+10;
int n,m,c,l;
int x,y,v;
int sum[N][M];
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d%d%d",&x,&y,&v);
sum[++x][++y]+=v;
if(x>c) c=x;
if(y>l) l=y;
}
for(int i=1;i<=c;i++) for(int j=1;j<=l;j++) sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
int ans=0,m1=std::min(m,c),m2=std::min(m,l);
for(int i=m1;i<=c;i++) for(int j=m2;j<=l;j++) ans=std::max(ans,sum[i][j]-sum[i-m1][j]-sum[i][j-m2]+sum[i-m1][j-m2]);
printf("%d\n",ans);
return 0;
}
差分
差分常用于降维,即将原序列上的“区间操作”转化为差分序列上的“单点操作”进行计算.
P4552 [Poetize6] IncDec Sequence
区间操作不好考虑,序列问题,考虑降维为单点操作.
求出 \(a\) 序列的差分序列 \(b\),令 \(b_{n+1}=0\),原题化归为每次选出 \(b_1,b_2,\cdots,b_{n+1}\) 中的任意两个数,一个加 \(1\) 一个减 \(1\),目标是把 \(b_2,b_3,\cdots,b_n\) 全部变成 \(0\),其中原序列取值取决于 \(b_1\) 的值.
因此有四种操作:
- 选 \(b_i\) 和 \(b_j\),其中 \(2 \leq i,j \leq n\),贪心来看,应保证 \(b_i\) 和 \(b_j\) 一正一负的情况下尽可能多的进行该操作;
- 选 \(b_1\) 和 \(b_i\),其中 \(2 \leq i \leq n\);
- 选 \(b_i\) 和 \(b_{n+1}\),其中 \(2 \leq i \leq n\);
- 选 \(b_1\) 和 \(b_{n+1}\),显然这种情况会浪费一次操作,无意义.
因此应尽量选择操作 1,故可以先统计 \(b_2,\cdots,b_n\) 中的正数之和与负数之和的绝对值,分别记为 \(p,q\),此时操作 \(1\) 至多可以执行 \(\min(p,q)\) 次.
剩余 \(|p-q|\) 可以采取操作 2 或操作 3,区别在于是否改变原序列最后统一的值.
因此最少操作数为 \(\min(p,q)+|p-q|=\max(p,q)\),加上完全不对 \(b_1\) 进行操作,最终的值会有 \(|p-q|+1\) 种.
for(int i=1;i<=n;i++) d[i]=a[i]-a[i-1];
for(int i=2;i<=n;i++) d[i]>0?sump+=d[i]:sumq+=d[i];
sumq*=-1;
printf("%lld\n%lld",std::max(sump,sumq),std::max(sump,sumq)-std::min(sump,sumq)+1);
P2879 [USACO07JAN] Tallest Cow S
本题有两个常见的技巧:
- 将大小关系极端化表示,如在本题中要求牛的身高尽可能高,又要满足大小关系,那么最小相差 \(1\) 即可;
- 将序列问题降维化归为点问题.
#include <map>
#include <cstdio>
#include <algorithm>
const int N=1e5+10;
std::map<std::pair<int,int>,bool> visd;
int n,id,h,m;
int d[N];
int main() {
scanf("%d%d%d%d",&n,&id,&h,&m);
for(int i=1;i<=m;i++) {
int a=0,b=0;
scanf("%d%d",&a,&b);
if(a>b) std::swap(a,b);
if(visd[std::make_pair(a+1,b)]) continue;
--d[a+1];++d[b];
visd[std::make_pair(a+1,b)]=true;
}
for(int i=1;i<=n;i++) d[i]+=d[i-1];
for(int i=1;i<=n;i++) printf("%d\n",h+d[i]);
return 0;
}
0x04 二分
二分常用于当答案具有单调性时,把求解转化为判定,根据复杂度理论,判定难度小于求解.
整数集合上的二分
该写法保证最终答案处于 \([l,r]\) 以内,循环以 \(l=r\) 结束.
while(l<r) {
int mid=(l+r)>>1;
if(chk(mid)) r=mid;
else l=mid+1;
}
while(l<r) {
int mid=(l+r+1)>>1;
if(chk(mid)) l=mid;
else r=mid-1;
}
需要注意的是:
- mid 的取法应该是配套的;
- 不可将右移运算更改为整数除法,前者是向下取整,后者是向零取整。
同时分析这两种 mid 的取法会发现第一种取不到 \(r\),第二种取不到 \(l\),因此可以利用这一性质来处理无解的情况.
把最初的区间 \([l,r]\) 分别扩大为 \([l,r+1]\) 和 \([l-1,r]\),当最终答案处于越界下标上时说明无解.
因此,写出正确的二分流程是:
- 分析具体问题,确定左右半段哪一个是可行区间,以及 mid 归属哪一半;
- 选择两种配套形式其中之一;
- 得出答案。
实数域上的二分
一般来讲需要保留 \(k\) 位小数时,确定好所需的精度 \(eps=10^{-(k+2)}\),然后以 \(l+eps<r\) 为循环条件,每次根据在 mid 上的判定选择 \(r=mid\) 或 \(l=mid\) 其中之一即可.
while(l+eps<r) {
double mid=(l+r)/2;
if(chk(mid)) l=mid;else r=mid;
/*if(chk(mid)) r=mid;else l=mid;*/
}
而精度不容易确定的情况下,可以采用循环次数固定的二分形式.
for(int i=1;i<=N;i++) {
double mid=(l+r)/2;
if(chk(mid)) l=mid;else r=mid;
/*if(chk(mid)) r=mid;else l=mid;*/
}
三分求单峰函数极值
需满足严格单调性.
在函数定义域 \([l,r]\) 上任取两个点 \(lmid, rmid\):
- 若 \(f(lmid)<f(rmid)\),则 \(lmid\) 与 \(rmid\) 要么同时处于极大值点左侧,要么处于极大值点两侧,此时极大值点一定在 \(lmid\) 右侧,令 \(l=lmid\);
- 同理,若 \(f(lmid)>f(rmid)\),则令 \(r=rmid\);
- 当 \(f(lmid)=f(rmid)\),令 \(l=lmid\) 或 \(r=rmid\) 均可.
while(l+eps<r) {
double mid=(l+r)/2;
double lmid=mid-eps,rmid=mid+eps;
if(lmid<rmid) l=mid;
else r=mid;
}
二分答案转化为判定
一个宏观的最优化问题也可以抽象为函数,其 “定义域” 是该问题下的可行方案,对这些可行方案进行评估的数值构成函数的值域,最优解就是评估值最优的方案.
假设最优解的评分为 \(S\),显然对于 \(\forall x>S\),都不存在一个可行的方案达到 \(x\) 分;同理,对于 \(\forall x \leq S\),一定存在一个合法的方案达到或超过 \(x\) 分.
因此这样的问题有一个特殊的单调性:在 \(S\) 的一侧合法,在 \(S\) 的另一侧不合法,可通过二分找到这个分界点 \(S\).
P10450 [USACO03MAR] Best Cow Fences G
二分答案,判定“是否存在一个长度不小于 \(L\) 的子段,平均数不小于二分的值”.
假设 \(S\) 是最优解,那么对于 \(\forall x \leq S\),必定会满足题意;对于 \(\forall x>S\),显然不满足,否则与 \(S\) 的最优性互悖.
因此该问题的答案具有单调性,可以二分答案.
如果把数列中每个数都减去二分的值,就转化为判定“是否存在一个长度不小于 \(L\) 的子段,子段和非负”.
而要求子段和非负的存在性问题,可以转化为子段和最大的问题,而子段和最大问题是一个经典问题,只需 \(O(n)\) 扫描数列,不断地把新的数加入子段,当子段和变为负数时清空子段,最后取最值即可.
加上长度限制,可以转化为前缀和相减的形式进行降维,观察可发现具有可继承性,即对于一个右端点,其左端点一定单调不减.
#include <cstdio>
#include <iostream>
#include <algorithm>
const int N=1e5+10;
const double eps=1e-5;
int n,m;
double a[N],sum[N];
bool chk(double x) {
for(int i=1;i<=n;i++) sum[i]=a[i]-x+sum[i-1];
double ans=-1e10,minn=1e10;
for(int i=m;i<=n;i++) {
minn=std::min(minn,sum[i-m]);
ans=std::max(ans,sum[i]-minn);
}
if(ans>=0) return true;
return false;
}
int main() {
scanf("%d%d",&n,&m);
double l=1e10,r=0;
for(int i=1;i<=n;i++) {
scanf("%lf",&a[i]);
l=std::min(l,a[i]);
r=std::max(r,a[i]);
}
while(l+eps<r) {
double mid=(l+r)/2;
if(chk(mid)) l=mid;
else r=mid;
}
std::cout<<int(r*1000);
return 0;
}
此处注意一个语法点:四舍五入统一用 cout 语句,格式如上.
根据数学归纳法(思考问题的角度++),若前 \(k-1\) 个元素已经按照要求排成一排,现插入第 \(k\) 个元素.
而这相当于找第 \(k\) 个元素的前驱并插入在其后,可以通过二分解决,证明如下:
- 若第 \(k\) 个元素比第 \(mid\) 个元素小,那么继续比较第 \(k\) 个元素与第 \(mid-1\) 个元素,若比其大,则插入在第 \(mid-1\) 个元素之后,反之继续比较第 \(k\) 个和第 \(mid-2\) 个,若发现比所有元素都小,那么放在最前面;
- 若第 \(k\) 个元素比第 \(mid\) 个元素大,同理,直到比所有元素都大,那么放在最后面.
实际上这证明了:任意有向完全图(竞赛图)都存在 Hamilton(哈密顿) 路径.
而通过二分则复杂度为 \(O(n \log n)\).
此处该类不到右端点,而当 \(r=i\) 时正好对应无解情况,也恰好插入在此处.
#include <cstdio>
#include <iostream>
using namespace std;
const int N=1e3+10;
bool compare(int a, int b)
{
cout << "? " << a << ' ' << b << endl;
bool t;
cin >> t;
return t;
}
int n;
int q[N];
int main() {
scanf("%d",&n);
q[1]=1;
for(int i=2;i<=n;i++) {
int l=1,r=i;
while(l<r) {
int mid=(l+r)>>1;
if(compare(i,q[mid])) r=mid;
else l=mid+1;
}
for(int j=i-1;j>=l;j--) q[j+1]=q[j];
q[l]=i;
}
printf("! ");
for(int i=1;i<=n;i++) printf("%d ",q[i]);
return 0;
}
0x05 排序
离散化
std::sort(lsh+1,lsh+n);
int m=std::unique(lsh+1,lsh+n)-lsh-1;
for(int i=1;i<=n;i++) a[i]=std::lower_bound(lsh+1,lsh+m+1,a[i])-lsh;
模板题.
中位数
一个很经典的模型,求形如 \(\min |\sum a_i-a_k|\).
此时将 \(a\) 从小到大排序后,\(k\) 应取中位数.
下证:
设货仓建在 \(X\) 坐标处,\(X\) 左侧的商店有 \(P\) 家,右侧的商店有 \(Q\) 家.
若 \(P<Q\),则向右移动一个单位距离距离之和会减少 \(Q-P\),同理,若 \(P>Q\),则向左移动一个单位距离距离之和会减少 \(P-Q\),当 \(P=Q\) 时为最优解,因此选中位数.
std::sort(a+1,a+1+n);
int ans=0;
for(int i=1;i<=(n+1)/2;i++) ans+=a[(n+1)/2]-a[i];
for(int i=(n+1)/2;i<=n;i++) ans+=a[i]-a[(n+1)/2];
首先可以发现一次操作不会同时影响到行和列的答案,因此可以两个问题分开考虑.
下以行为例.
而若先从序列开始考虑起,会发现这是一个经典问题:均分纸牌.
有解的条件显然为 \(n \mid t\).
而会发现,从左往右考虑,纸牌的流向和数目是一定的.
因此,对于第 \(i\) 个人,必定会有 \(\sum |b_j-\frac{t}{n}|\) 张卡牌是流经第 \(i\) 个人的,因此就可以得到最少步数为:
而若假设从一开始就减去了最终的卡牌数,那么可以记为:
而如果是环,常用的就是断环成链这个 trick,因此我们可以枚举断点,推导可得所需最小步数为:
会发现这是货仓选址的经典模型,只需排序后取中位数即可.
因此总复杂度为 \(O(N \log N+M \log M)\).
#include <cstdio>
#include <algorithm>
typedef long long ll;
const int N=1e5+10;
const int M=1e5+10;
const int T=1e5+10;
int n,m,t;
ll sum[2][N];
ll calc1() {
for(int i=1;i<=n;i++) sum[0][i]-=t/n;
for(int i=1;i<=n;i++) sum[0][i]+=sum[0][i-1];
std::sort(sum[0]+1,sum[0]+1+n);
ll res=0;
for(int i=1;i<=(n+1)/2;i++) res+=sum[0][(n+1)/2]-sum[0][i];
for(int i=(n+1)/2;i<=n;i++) res+=sum[0][i]-sum[0][(n+1)/2];
return res;
}
ll calc2() {
for(int i=1;i<=m;i++) sum[1][i]-=t/m;
for(int i=1;i<=m;i++) sum[1][i]+=sum[1][i-1];
std::sort(sum[1]+1,sum[1]+1+m);
ll res=0;
for(int i=1;i<=(m+1)/2;i++) res+=sum[1][(m+1)/2]-sum[1][i];
for(int i=(m+1)/2;i<=m;i++) res+=sum[1][i]-sum[1][(m+1)/2];
return res;
}
int main() {
scanf("%d%d%d",&n,&m,&t);
for(int i=1;i<=t;i++) {
int x=0,y=0;
scanf("%d%d",&x,&y);
++sum[0][x];++sum[1][y];
}
if(!(t%n) && !(t%m)) printf("both %lld\n",calc1()+calc2());
else if(!(t%n)) printf("row %lld\n",calc1());
else if(!(t%m)) printf("column %lld\n",calc2());
else puts("impossible");
return 0;
}
动态维护中位数问题,可以有两种做法,对顶堆的在线做法和链表结合 Hash 的离线做法.
下面说明对顶堆做法:
维护两个二叉堆,一个小根堆一个大根堆.
在依次读入这个序列的同时,我们始终保持从小到大排名前一半的整数存储在大根堆中,另一半存储在小根堆中,如果一个堆的元素过多就取出堆顶放到另一个堆中,此时序列的中位数就是小根堆的堆顶.
每读入一个数后就按和当前中位数的大小关系加入大根堆或小根堆.
#include <cstdio>
#include <queue>
const int N=1e5+10;
int n,x,tot1,tot2;
std::priority_queue<int> p,q;
int main() {
scanf("%d",&n);
scanf("%d",&x);
q.push(-x);tot2=1;
printf("%d\n",x);
for(int i=2;i<=n;i++) {
scanf("%d",&x);
if(x<-q.top()) {p.push(x);++tot1;}
else {q.push(-x);++tot2;}
while(tot2-tot1>1) {p.push(-q.top());q.pop();++tot1;--tot2;}
while(tot1-tot2>0) {q.push(-p.top());p.pop();--tot1;++tot2;}
if(i&1) printf("%d\n",-q.top());
}
return 0;
}
第 k 大数
最直接的想法是 \(O(n \log n)\) 排序后输出 k-th.
但利用快速排序的思想可以做到 \(O(n)\).
首先快速排序在每次排序时会选取一个基准值,然后将其分成两个部分,小于基准值和大于基准值,基准值所在位置为分界线.
用三路快速排序和随机取基准值可以有效地优化为 \(O(n \log n)\).
结合分治的思想,可以很好的做到 \(O(n)\) 求出 k-th.
#include <ctime>
#include <random>
#include <cstdio>
#include <algorithm>
const int N=5e6+10;
int sqrand(int l,int r) {
int len=r-l+1;
return l+(rand()%len);
}
int kth(int a[],int s,int t,int k) {
if(s>=t) return a[s];
int x=sqrand(s,t);
int p=s,i=s+1,q=t+1,base=a[x];
std::swap(a[x],a[s]);
while(i<q) {
if(a[i]<base) {
std::swap(a[i],a[p+1]);++p;++i;
} else if(a[i]>base) {
std::swap(a[i],a[q-1]);--q;
} else ++i;
}
std::swap(a[s],a[p]);
if(k<=p-s+1) return kth(a,s,p,k);
else if(k>q-s) return kth(a,q,t,k-q+s);
else return base;
}
int n,k;
int a[N];
int main() {
srand(time(0));
scanf("%d%d",&n,&k);++k;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
printf("%d\n",kth(a,1,n,k));
return 0;
}
模板题.
逆序对
常用树状数组或归并排序求解.
归并排序本质上是分治,重点在如何合并两个有序数组,这里用两个指针进行合并即可.
在合并的过程中,若发现右半部分的数组中所指的元素小于左半部分的数组中所指的元素,则说明 \(i \sim mid\) 都与 \(j\) 构成逆序对,可以累加贡献.
#include <cstdio>
typedef long long ll;
const int N=5e5+10;
int b[N];
ll merge(int a[],int l,int mid,int r) {
int i=l,j=mid+1;
ll res=0;
for(int k=l;k<=r;k++) {
if(j>r || (i<=mid && a[i]<=a[j])) b[k]=a[i++];
else b[k]=a[j++],res+=mid-i+1;
}
for(int k=l;k<=r;k++) a[k]=b[k];
return res;
}
ll msort(int a[],int l,int r) {
if(l==r) return 0;
int mid=(l+r)>>1;
ll res=0;
res+=msort(a,l,mid);
res+=msort(a,mid+1,r);
return res+merge(a,l,mid,r);
}
int n;
int a[N];
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
printf("%lld\n",msort(a,1,n));
return 0;
}
模板题.
当且仅当在遇到一个大小顺序不符的就进行交换,否则无法完成排序或不够优,因此等价于求逆序对数量.
遇到与交换可行性/次数有关的问题可以考虑逆序对.
写成一个序列,可以发现在该问题中,若左右交换,逆序对数量显然不变,若上下交换,可以发现会有 \(n-1\) 个数与被交换数的大小关系发生了改变,但 \(n-1\) 是偶数,因此交换并不会导致逆序对的奇偶性发生改变.
故只需判断两种状态的逆序对奇偶性是否一致即可.
其中逆序对相关问题一般要开 long long.
#include <cstdio>
typedef long long ll;
const int N=500+10;
int c[N*N];
ll merge(int a[],int l,int mid,int r) {
int i=l,j=mid+1;
ll res=0;
for(int k=l;k<=r;k++) {
if(j>r || (i<=mid && a[i]<=a[j])) c[k]=a[i++];
else {c[k]=a[j++];res+=mid-i+1;}
}
for(int k=l;k<=r;k++) a[k]=c[k];
return res;
}
ll msort(int a[],int l,int r) {
if(l==r) return 0;
int mid=(l+r)>>1;
ll res=0;
res+=msort(a,l,mid);
res+=msort(a,mid+1,r);
res+=merge(a,l,mid,r);
return res;
}
int n;
int a[N*N],b[N*N];
int main() {
while(scanf("%d",&n)!=EOF) {
int i=1;
for(int k=1;k<=n*n;k++) {
scanf("%d",&a[i++]);
if(!a[i-1]) --i;
}
i=1;
for(int k=1;k<=n*n;k++) {
scanf("%d",&b[i++]);
if(!b[i-1]) --i;
}
if(n==1) {puts("TAK");continue;}
if((msort(a,1,n*n-1)&1)!=(msort(b,1,n*n-1)&1)) puts("NIE");
else puts("TAK");
}
return 0;
}
而若 \(n\) 为偶数,可以发现上下交换时发生改变的 \(n-1\) 对关系变为了奇数,此时要是进行了奇数次上下变换,逆序对奇偶性改变,反之不变.
由于最后空格的位置是相同的,因此上下交换次数的奇偶性是一定的,故最后逆序对的奇偶性也是与之对应的,若上下交换次数为奇数,此时需满足两个序列的逆序对奇偶性不同;若上下交换次数为偶数,则需满足两个序列的逆序对奇偶性相同.
概括来说,即需满足逆序对之差和空格所在行数之差的奇偶性相同.
因此可以扩展到 \(n \times m\) 的数码问题,取决于 \(m\) 的奇偶性.
0x06 倍增
二分和倍增都基于单调性,但二分上界不确定的情况下最优解是倍增,可以有效避免极端情况.
首先贪心地想,本质上这是一个恒成立问题,要使得 \(SPD \leq k\) 恒成立,只需满足 \((SPD)_{max} \leq k\) 恒成立.
因此问题化归为划分区间,满足 \((SPD)_{max} \leq k\) 的同时,\((SPD)_{max}\) 尽可能大.
因此将第 \(k\) 大与第 \(k\) 小进行匹配即可.
观察到,在 \(l\) 固定的情况下,\(SPD\) 单调不减,因此可以用二分/倍增,此处为了避免极端数据,用倍增比较妥当.
每次再判断是否可行的时候,将该段排序并检验即可,时间复杂度 \(O(n \log^2 n)\).
而观察会发现,倍增的时候只需要对增加的部分进行排序,然后按归并排序的方式进行合并即可,此时时间复杂度可以降到 \(O(n \log n)\).
注意在排序的时候要另外用一个数组,保存原数组,在检验成功时再进行拷贝.
#include <cstdio>
#include <algorithm>
typedef long long ll;
const int N=5e5+10;
int m1[N],m2[N];
void merge(int a[],int l,int mid,int r) {
int i=l,j=mid+1;
for(int k=l;k<=r;k++) {
if(j>r || (i<=mid && a[i]<=a[j])) m2[k]=a[i++];
else m2[k]=a[j++];
}
for(int k=l;k<=r;k++) a[k]=m2[k];
}
void msort(int a[],int l,int r) {
if(l==r) return;
int mid=(l+r)>>1;
msort(a,l,mid);msort(a,mid+1,r);
merge(a,l,mid,r);
}
int t;
int n,m;
ll lim;
int p[N];
bool chk(int l,int mid,int r) {
for(int i=mid+1;i<=r;i++) m1[i]=p[i];
msort(m1,mid+1,r);
int i=l,j=mid+1;
for(int k=l;k<=r;k++) {
if(j>r || (i<=mid && m1[i]<=m1[j])) m2[k]=m1[i++];
else m2[k]=m1[j++];
}
ll res=0;
for(i=l,j=r;i<l+m && i<j;i++,j--) res+=1ll*(m2[i]-m2[j])*(m2[i]-m2[j]);
return res<=lim;
}
void sol() {
scanf("%d%d%lld",&n,&m,&lim);
for(int i=1;i<=n;i++) scanf("%d",&p[i]);
int l=1,r=1,ans=0;
m1[1]=m2[1]=p[1];
while(r<=n) {
int del=1;
while(del) {
if(r+del<=n && chk(l,r,r+del)) {
r+=del;del<<=1;
if(r>=n) break;
for(int i=l;i<=r;i++) m1[i]=m2[i];
} else del>>=1;
}
l=r;++ans;
++l;++r;
m1[l]=m2[l]=p[l];
}
printf("%d\n",ans);
}
int main() {
scanf("%d",&t);
while(t--) sol();
return 0;
}
ST 算法
倍增在求解 RMQ 问题中的应用.
#include <cstdio>
#include <algorithm>
template<class T>
inline void read(T &val) {
T x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {ch=='-'?f=-1:0;ch=getchar();}
while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
val=x*f;
}
template<class T>
inline void write(T x) {
x<0?x=-x,putchar('-'):0;
if(x>9) write(x/10);
putchar(x%10^48);
}
const int N=1e5+10;
const int W=17;
int n,m;
int f[N][W];
int lg[N];
int main() {
read(n);read(m);
for(int i=1;i<=n;i++) read(f[i][0]);
for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
for(int j=1;j<=W;j++) for(int i=1;i+(1<<j)-1<=n;i++) f[i][j]=std::max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
while(m--) {
int l=0,r=0;
scanf("%d%d",&l,&r);
int s=lg[r-l+1];
write(std::max(f[l][s],f[r-(1<<s)+1][s]));
putchar('\n');
}
return 0;
}
0x07 贪心
贪心要求问题的整体最优性可以由局部最优性导出,贪心算法的正确性常见的证明手段有:
- 微扰(邻项交换):证明在任意局面下,任何对局部最优策略的微小改变都会造成整体结果变差。经常用于以“排序”为贪心策略的证明;
- 范围缩放:证明任何对局部最优策略作用范围的扩展都不会造成整体结果变差;
- 决策包容性:证明在任意局面下,做出局部最优决策以后,在问题状态空间中的可达集合包含了作出其他任何决策后的可达集合。换言之,这个局部最优策略提供的可能性包含其他所有策略提供的可能性;
- 反证法;
- 数学归纳法.
P2887 [USACO07NOV] Sunscreen G
按照 \(minSPF\) 递减的顺序把奶牛排序,依次考虑每头奶牛.
对于每头奶牛,扫描一遍所有的防晒霜,使用可用的且 \(SPF\) 最大的.
下证该算法的正确性:
排序后,会保证每一个不低于当前奶牛 \(minSPF\) 的防晒霜,都不会低于后面其他奶牛的 \(minSPF\),因此,对于当前奶牛可用的任意两瓶防晒霜 \(x\) 和 \(y\),如果 \(SPF_x<SPF_y\),那么后面其他奶牛只可能出现两瓶均能用、均不能用或者只能用 \(x\) 不能用 \(y\),即范围缩放.
同时每头奶牛对答案的贡献至多是 \(1\),即使让当前奶牛放弃,在后续对答案的贡献也不会更大,即决策包容性,因此贪心策略正确.
#include <cstdio>
#include <algorithm>
const int N=2500+10;
const int M=2500+10;
struct cow {
int l,r;
}G[N];
int n,m,ans;
int spf[M],num[M];
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&G[i].l,&G[i].r);
std::sort(G+1,G+1+n,[](cow a,cow b) {
return a.l>b.l;
});
for(int i=1;i<=m;i++) scanf("%d%d",&spf[i],&num[i]);
for(int i=1;i<=n;i++) {
int p=0;
for(int j=1;j<=m;j++) {
if(spf[j]<G[i].l || spf[j]>G[i].r) continue;
if((!p || spf[j]>spf[p]) && num[j]) p=j;
}
if(p) --num[p],++ans;
}
printf("%d\n",ans);
return 0;
}
P2859 [USACO06FEB] Stall Reservations S
按照开始吃草的时间从小到大排序.
每次尝试安排在结束时间最早的栅栏中,失败则新建栅栏,用小根堆维护时间复杂度为 \(O(n \log n)\).
下面证明这个贪心策略是正确的:
对于每一头奶牛,开始时间是一定的,因此从小到大排序后,安排在最早结束的栅栏中时一定不会影响到后面的奶牛,对于每一头奶牛,因为结束时间是一定的,因此安排在任意一个符合条件的栅栏所导致的结果是一样的,即满足决策包容性,而对于每一个栅栏,若后面那一头奶牛开始时间严格大于前一头,对于每个栅栏会有三种情况出现,两头奶牛均可使用,两头奶牛均不可使用,第二头奶牛可以使用而第一头奶牛不能使用,因此选择任意一个可以使用的栅栏一定不会使得答案更劣,即满足范围缩放,因此该贪心策略正确.
#include <queue>
#include <cstdio>
#include <algorithm>
const int N=5e4+10;
struct cow {
int st,ed,id;
}G[N];
int n;
int ans[N];
std::priority_queue<std::pair<int,int>> q;
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&G[i].st,&G[i].ed);
for(int i=1;i<=n;i++) G[i].id=i;
std::sort(G+1,G+1+n,[](cow a,cow b) {
return a.st<b.st;
});
int cnt=0;
for(int i=1;i<=n;i++) {
if(!cnt) {
q.push(std::make_pair(-G[i].ed,++cnt));
ans[G[i].id]=cnt;
continue;
}
auto p=q.top();
if(-p.first<G[i].st) {
q.pop();
ans[G[i].id]=p.second;
p=std::make_pair(-G[i].ed,p.second);
q.push(p);
} else {
ans[G[i].id]=++cnt;
q.push(std::make_pair(-G[i].ed,cnt));
}
}
printf("%d\n",cnt);
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}
对于每个建筑物,可以计算出 \(x\) 轴上的一段能管辖它的区间 \(l_i \sim r_i\).
问题化归为给定 \(n\) 个区间,在 \(x\) 轴上放置最少的点,使每个区间包含至少一个点.
按照每个区间的左端点从小到大排序,用一个变量维护已经安放的最后一台监视器的坐标 \(pos\).
对于第 \(i\) 个建筑,如果当前设备可以监视得到,即 \(l_i \leq pos\),则令该监视器的位置尽可能地靠后,即 \(pos=\min(pos,r_i)\).
反之,则新建一个尽可能靠后地监视器,令 \(pos=r_i\).
下面证明该贪心策略正确:
对于每个区间有两种选择,使用已有的设备监视和新建一台设备监视.
而选择前者一定不劣于选择后者,因为选择前者后,我们在未来可以在任意地方新建一台监控设备,而选择后者只能在区间范围内新建设备,前者可能到达的状态包含了后者,因此根据决策包容性可知正确.
同时,尽可能往后放也包含了放在更靠前的位置可达的状态,同时由于 \(l_i\) 升序,所以根据决策包容性,该策略也是正确的,因此该贪心策略正确.
#include <cmath>
#include <cstdio>
#include <algorithm>
const int N=1e3+10;
struct island {
double l,r;
}G[N];
int n,d,k;
void sol() {
++k;
bool f=true;
scanf("%d%d",&n,&d);
if(!n && !d) exit(0);
for(int i=1;i<=n;i++) {
int x=0,y=0;
scanf("%d%d",&x,&y);
if(!f) continue;
double del=d*d-y*y;
if(del<0 || d<0) {f=false;continue;}
del=sqrt(del);
G[i].l=x-del;
G[i].r=x+del;
}
if(!f) {
printf("Case %d: -1\n",k);
return;
}
std::sort(G+1,G+1+n,[](island a,island b) {
return a.l<b.l;
});
double pos=-0x3f3f3f3f;
int cnt=0;
for(int i=1;i<=n;i++) {
if(G[i].l>pos) {pos=G[i].r;++cnt;}
else pos=std::min(pos,G[i].r);
}
printf("Case %d: %d\n",k,cnt);
}
int main() {
while(1) sol();
return 0;
}
贪心策略:按每个大臣左右手上的乘积从小到大排序,所得队伍即为最优答案.
可以用微扰证明:
若交换两个相邻的大臣 \(i\) 与 \(i+1\),在交换前这两个大臣获得的奖励值为 \(\frac{1}{b_i} \prod_{j=0}^{i-1} a_j\) 与 \(\frac{1}{b_{i+1}}\prod_{j=0}^i a_j\).
交换后两个相邻的大臣获得的奖励值为 \(\frac{1}{b_{i+1}}\prod_{j=0}^{i-1} a_j\) 与 \(\frac{a_{i+1}}{b_i} \prod_{j=0}^{i-1} a_j\).
其他大臣获得的奖励都不变,因此取决于这两个大臣.
交换前的答案为 \(\max(\frac{1}{b_i} \prod_{j=0}^{i-1} a_j,\frac{1}{b_{i+1}}\prod_{j=0}^i a_j)\),交换后的答案为 \(\max(\frac{1}{b_{i+1}}\prod_{j=0}^{i-1} a_j,\frac{a_{i+1}}{b_i} \prod_{j=0}^{i-1} a_j)\).
由于 \(a,b\) 均为正数,同时除以 \(\prod_{j=0}^{i-1} a_j\) 并乘上 \(b_i b_{i+1}\) 得到比较 \(\max(b_{i+1},a_ib_i)\) 与 \(\max(b_i,a_{i+1}b_{i+1})\).
因为 \(a_ib_i>b_i\),\(a_{i+1}b_{i+1}>b_{i+1}\),所以当 \(a_ib_i<a_{i+1}b_{i+1}\) 时,交换前更优,反之交换后更优.
即无论如何,尽可能地减少逆序对数量一定不劣.
因此该贪心策略得证.
#include <cstdio>
#include <algorithm>
template<class T>
inline void read(T &val) {
T x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {ch=='-'?f=-1:0;ch=getchar();}
while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
val=x*f;
}
template<class T>
inline void write(T x) {
x<0?x=-x,putchar('-'):0;
if(x>9) write(x/10);
putchar(x%10^48);
}
typedef long long ll;
const int N=1e3+10;
struct node {
__int128 a,b;
}G[N];
int n;
__int128 ans=0;
int main() {
read(n);
for(int i=0;i<=n;i++) read(G[i].a),read(G[i].b);
std::sort(G+1,G+1+n,[](node a,node b) {
return a.a*a.b<b.a*b.b;
});
__int128 mul=G[0].a;
for(int i=1;i<=n;i++) {
ans=std::max(ans,mul/G[i].b);
mul*=G[i].a;
}
write(ans);
return 0;
}
首先需要观察到一个性质:树中点权最大的结点一定在父节点染色之后立即染色.
设树中点权最大的节点权值为 \(a\),父节点是第 \(k-1\) 次染色,在父节点染色后可供染色的任意结点点权为 \(b\),必有 \(a > b\).
若立即染色,则贡献为 \(ka+(k+1)b\),反之贡献为 \(kb+(k+1)a\),同时减去 \(ka+kb\),则转化为比较 \(b\) 与 \(a\) 的大小,显然前者更优,因此可得该节点越早染色越好.
所以可以考虑将该结点和它的父节点合并为一个结点,记等价权值为 \(z\),该权值需满足我们上面所提及的性质.
设有点权分别为 \(a,b,c\) 的三个结点,其中前两个结点合并,那么若 \(c\) 先被染色,则贡献为 \(kc+(k+1)a+(k+2)b\),反之则贡献为 \(ka+(k+1)b+(k+2)c\),前者减后者为 \(a+b-2c\),因此关键就在于 \(a+b\) 与 \(2c\) 的大小关系,前者大则 \(c\) 慢被染色更优,因此等价权值 \(z=\frac{a+b}{2}\).
而我们采用确定顺序的同时不断累加贡献的方式即可.
再用并查集和堆维护即可,复杂度 \(O(n \log n)\).
#include <queue>
#include <cstdio>
#include <algorithm>
const int N=1e3+10;
struct UFDS {
int fa[N],siz[N],val[N];
void init(int n) {
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++) siz[i]=1;
for(int i=1;i<=n;i++) scanf("%d",&val[i]);
}
int f(int x) {
if(fa[x]==x) return x;
return fa[x]=f(fa[x]);
}
void uni(int x,int y) {
int fx=f(x),fy=f(y);
if(fx==fy) return;
fa[fy]=fx;
siz[fx]+=siz[fy];
val[fx]+=val[fy];
}
}s;
std::priority_queue<std::pair<double,int>> q;
int n,rt,ans;
int fa[N];
bool vis[N];
void sol() {
scanf("%d%d",&n,&rt);
if(!n && !rt) exit(0);
s.init(n);ans=0;
for(int i=1;i<n;i++) {
int u=0,v=0;
scanf("%d%d",&u,&v);
fa[v]=u;
}
for(int i=1;i<=n;i++) {
ans+=s.val[i];
vis[i]=false;
if(i==rt) continue;
q.push(std::make_pair(s.val[i],i));
}
while(!q.empty()) {
auto cur=q.top();q.pop();
int p=cur.second;
if(vis[p]) continue;
vis[p]=true;
int fx=s.f(fa[p]),fy=s.f(p);
ans+=s.siz[fx]*s.val[fy];
s.uni(fx,fy);
if(fx!=rt) q.push(std::make_pair(1.0*s.val[fx]/s.siz[fx],fx));
}
printf("%d\n",ans);
}
int main() {
while(1) sol();
return 0;
}

浙公网安备 33010602011771号