AGC023F 题解

传送门

Description

给定一棵 \(n\) 个点的外向树,每个节点上有一个 \(0/1\) 的权值,求一个节点拓扑序使得节点权值的逆序对个数最小,输出最小值。 \(n \le 2 \times 10^5\)

Solution

首先,对于一个 \(0\) 的节点,可以把其和父亲绑在一起,选了其父亲后立即就选它。

这样可以把整棵树分成若干连通块,每个连通块仅有深度最小的节点为 \(1\) 其余全 \(0\),然后对这些连通块进行拓扑排序。为了使逆序对数最小,显然应该优先让 \(sz\) 大的连通块在序列中排在前面。

然后假了。

因为可能有一个小连通块,后面跟着一个大连通块,但是整个一坨东西选的比较晚导致答案不优。

我们发现拓扑排序本质就是把连通块按照一定次序往根节点上合并。发现连通块可以不和根节点合并,只和父亲所在的连通块合并,这样会先把大连通块与小连通块合并成更优的连通块,使其选的早一些,保证了决策正确性。

现在只需给出大连通块的定义(即排在前面的连通块具有的特征)。将连通块中 \(0\) 的个数记为 \(cnt_0\)\(1\) 的个数记为 \(cnt_1\),那么当前连通块更优当且仅当 \(cnt_1 \times cnt_0'<cnt_0 \times cnt_1'\)。也就是邻项交换的思想。

可以把散点当作连通块进行合并。合并两个连通块时,相当于把一个接在另一个后面,可以直接统计这次合并对答案的贡献。

使用并查集维护 \(cnt_0\)\(cnt_1\),用优先队列维护连通块即可。复杂度 \(O(n \log n)\)

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,p[N],ans;
int cnt0[N],cnt1[N];
int fa[N];
bool vis[N];
int find(int x){return (fa[x]==x)?x:fa[x]=find(fa[x]);}
void merge(int x,int y)
{
    x=find(x),y=find(y);
    if(x==y)return ;
    fa[y]=x;
    ans+=cnt1[x]*cnt0[y];
    cnt0[x]+=cnt0[y];
    cnt1[x]+=cnt1[y];
    cnt0[y]=cnt1[y]=0;
}
struct node
{
    int num,x,y;
    bool operator <(const node &t)const
    {
        return x*t.y<y*t.x;
    }
};
priority_queue<node>q;
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=2;i<=n;i++)cin>>p[i];
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        fa[i]=i;
        if(x==0)cnt0[i]=1;
        else cnt1[i]=1;
        q.push({i,cnt0[i],cnt1[i]});
    }
    while(!q.empty())
    {
        auto [x,tmp1,tmp2]=q.top();q.pop();
        if(vis[x])continue;
        vis[x]=1;
        if(p[x]==0)continue;
        int nxt=find(p[x]);
        merge(nxt,x);
        q.push({nxt,cnt0[nxt],cnt1[nxt]});
    }
    cout<<ans<<"\n";

    return 0;
}

总结:

  1. 树上的贪心如果过于“目光短浅”,可以使用合并连通块的方式优化决策。
  2. 拓扑序可以通过捆绑节点与其前驱来优化答案。
  3. 邻项交换真的很神奇。
posted @ 2025-09-24 23:39  Antares_qwq  阅读(13)  评论(0)    收藏  举报