QOJ9904 最小生成树

QOJ9904 最小生成树

有趣的图论。

思路

a 排序,优先连接较小的 a 所表示边权的边。

并查集维护暴力连接是 O(n2) 的,显然不可以接受。

我们观察一下性质。

发现对于 ai 来说,对应了一个满足 l+r=n 的最大的区间 [l,r],其中 l,l+1,l+2,,(l+r)/2,依次与 r,r1,r2,,(l+r)/2+1 连边,下文 [l,r] 无特殊说明同上文。

由于只会连接 n1 条边,我们从每次连接的边的边权考虑。

做法一:

定义 fi 为点 i 并查集的根,初始时 fi=i

f 翻转,设为 f,即 fi=fni+1

那么对于 ai,找出 f[l,(l+r)/2]f[nr+1,n((l+r)/2+1)+1] 不相等的位置,就可以连上一条边。

ff 哈希,然后二分 ff 相同的长度,哈希判断。

哈希使用树状数组维护,并查集合并时按秩合并,且更新 ff 数组与其哈希值。

哈希查询修改一次 O(logn),二分找一条边查询哈希 logn 次,找边复杂度 O(nlog2n)

按秩合并遍历 nlogn 个点,修改总复杂度 O(nlog2n)

总复杂度 O(nlog2n)

#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 层,当 1in 时,f[i][k] 表示包括 i 向后 2k 个点与 f[i][k] 向前/后 2k 个点相同;当 n+1i2×n 时,f[i][k] 表示包括 i 向前 2k 个点与 f[i][k] 向前/后 2k​ 个点相同。

注意这里的前后仅取决于 i 是否小于等于 n

k=0 时,若 inf[i][k]=i;否则,f[i][k]=in

k>0 时,f[i][k]=i

每次查询时并查集需要返回,通过 ai[l,(l+r)/2][r,(l+r)/2+1] 之间新连接了多少条边。

查询 (l,r,k),若 f[l][k]f[r][k] 不属于同一个并查集,我们向 (l,r,k1)(l+2k1,r2k1,k1) 查询并将 f[l][k]f[r][k] 合并。

k=0lr 还是不在同一个块内,那么就需要连一条边。

每一层的有 n 个节点,最多两两合并 n1 次,一共 logn 层,复杂度 O(nlogn)

递归常数稍微大点。

#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);
}
posted @   彬彬冰激凌  阅读(68)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示