QOJ9904 最小生成树
QOJ9904 最小生成树
有趣的图论。
思路
将 \(a\) 排序,优先连接较小的 \(a\) 所表示边权的边。
并查集维护暴力连接是 \(O(n^2)\) 的,显然不可以接受。
我们观察一下性质。
发现对于 \(a_i\) 来说,对应了一个满足 \(l+r=n\) 的最大的区间 \([l,r]\),其中 \(l,l+1,l+2,\dots,(l+r)/2\),依次与 \(r,r-1,r-2,\cdots,(l+r)/2+1\) 连边,下文 \([l,r]\) 无特殊说明同上文。
由于只会连接 \(n-1\) 条边,我们从每次连接的边的边权考虑。
做法一:
定义 \(f_i\) 为点 \(i\) 并查集的根,初始时 \(f_i=i\)。
将 \(f\) 翻转,设为 \(f'\),即 \(f'_i=f_{n-i+1}\)。
那么对于 \(a_i\),找出 \(f[l,(l+r)/2]\) 与 \(f'[n-r+1,n-((l+r)/2+1)+1]\) 不相等的位置,就可以连上一条边。
将 \(f\) 与 \(f'\) 哈希,然后二分 \(f\) 与 \(f'\) 相同的长度,哈希判断。
哈希使用树状数组维护,并查集合并时按秩合并,且更新 \(f\) 与 \(f'\) 数组与其哈希值。
哈希查询修改一次 \(O(\log n)\),二分找一条边查询哈希 \(\log n\) 次,找边复杂度 \(O(n\log^2 n)\)。
按秩合并遍历 \(n\log n\) 个点,修改总复杂度 \(O(n\log^2 n)\)。
总复杂度 \(O(n\log^2 n)\)。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define mod 998244353
#define base 20090327
#define pii pair<int,int>
#define fi first
#define se second
const int maxn=4e5+5;
int n;
int fa[maxn];
pii a[maxn];
ll pw[maxn];
vector<int>vec[maxn];
struct treearray
{
ll ts[maxn];
inline int lowbit(int x){return x&(-x);}
inline void add(int x,ll v){v=(v+mod)%mod*pw[x]%mod;for(;x<=n;x+=lowbit(x)) (ts[x]+=v)%=mod;}
inline ll query(int x){ll sum=0;for(;x;x-=lowbit(x)) (sum+=ts[x])%=mod;return sum;}
inline ll qry(int l,int r){return (query(r)-query(l-1)+mod)%mod*pw[n-l]%mod;}
}L,R;
inline void init()
{
pw[0]=1;
for(int i=1;i<=n;i++) pw[i]=pw[i-1]*base%mod;
}
inline void updata(int x,int v){L.add(x,v-fa[x]);R.add(n-x+1,v-fa[x]);fa[x]=v;}
inline void merge(int x,int y)
{
x=fa[x],y=fa[y];
if(vec[x].size()<vec[y].size()) swap(x,y);
for(auto v:vec[y]) updata(v,x),vec[x].emplace_back(v);
}
inline pii check(int l,int r)
{
if(L.qry(l,r)==R.qry(n-r+1,n-l+1)) return {0,0};
int LL=1,rr=r-l+1;
while(LL<rr)
{
int mid=(LL+rr)>>1;
if(L.qry(l,l+mid-1)==R.qry(n-r+1,n-r+mid)) LL=mid+1;
else rr=mid;
}
return {l+LL-1,r-rr+1};
}
int main()
{
scanf("%d",&n);
init();
for(int i=3;i<n*2;i++) scanf("%d",&a[i].fi),a[i].se=i;
sort(a+3,a+n*2);
for(int i=1;i<=n;i++) updata(i,i),vec[i].emplace_back(i);
ll ans=0;
for(int i=3;i<n*2;i++)
{
pii tmp={0,0};
while((tmp=check(max(a[i].se-n,1),min(a[i].se-1,n))).fi)
merge(tmp.fi,tmp.se),ans+=a[i].fi;
}
printf("%lld",ans);
}
做法二:
如果你做过 SCOI2016 萌萌哒 这样的并查集合并一段的形式非常相像,这题中我们同样使用倍增来维护并查集。
对于第 \(k\) 层,当 \(1\leq i\leq n\) 时,\(f[i][k]\) 表示包括 \(i\) 向后 \(2^k\) 个点与 \(f[i][k]\) 向前/后 \(2^k\) 个点相同;当 \(n+1\leq i\leq 2\times n\) 时,\(f[i][k]\) 表示包括 \(i\) 向前 \(2^k\) 个点与 \(f[i][k]\) 向前/后 \(2^k\) 个点相同。
注意这里的前后仅取决于 \(i\) 是否小于等于 \(n\)。
\(k=0\) 时,若 \(i\leq n\),\(f[i][k]=i\);否则,\(f[i][k]=i-n\)。
\(k>0\) 时,\(f[i][k]=i\)。
每次查询时并查集需要返回,通过 \(a_i\),\([l,(l+r)/2]\) 与 \([r,(l+r)/2+1]\) 之间新连接了多少条边。
查询 \((l,r,k)\),若 \(f[l][k]\) 与 \(f[r][k]\) 不属于同一个并查集,我们向 \((l,r,k-1)\) 与 \((l+2^{k-1},r-2^{k-1},k-1)\) 查询并将 \(f[l][k]\) 与 \(f[r][k]\) 合并。
当 \(k=0\) 时 \(l\) 与 \(r\) 还是不在同一个块内,那么就需要连一条边。
每一层的有 \(n\) 个节点,最多两两合并 \(n-1\) 次,一共 \(\log n\) 层,复杂度 \(O(n\log n)\)。
递归常数稍微大点。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define fi first
#define se second
const int maxn=4e5+5;
struct DSU
{
int f[maxn];
inline void init(int n){for(int i=1;i<=n*2;i++) f[i]=i;}
inline int fr(int u){return u==f[u]?u:f[u]=fr(f[u]);}
inline bool merge(int u,int v){u=fr(u),v=fr(v);if(u==v) return 0;f[u]=v;return 1;}
}F[20];
int n;
int lg[maxn];
pii a[maxn];
inline void init(){for(int i=2;i<=n*2;i++) lg[i]=lg[i>>1]+1;}
inline int merge(int x,int y,int k)
{
if(!F[k].merge(x,y+n)) return 0;
if(k==0) return 1;
return merge(x,y,k-1)+merge(x+(1<<(k-1)),y-(1<<(k-1)),k-1);
}
int main()
{
scanf("%d",&n);
init();
for(int i=3;i<n*2;i++) scanf("%d",&a[i].fi),a[i].se=i;
sort(a+3,a+n*2);
for(int i=0;i<=lg[n];i++)
{
F[i].init(n);
if(i==0) for(int j=n+1;j<=n*2;j++) F[i].f[j]=j-n;
}
ll ans=0;
for(int i=3;i<n*2;i++)
{
int l=max(a[i].se-n,1),r=min(a[i].se-1,n);
int len=(r-l+1)/2,k=lg[len];
int l1=l,r1=l+len-1,l2=r-len+1,r2=r;
ans+=1ll*(merge(l1,r2,k)+merge(r1-(1<<k)+1,l2+(1<<k)-1,k))*a[i].fi;
}
printf("%lld",ans);
}

浙公网安备 33010602011771号