传送门
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;
}
总结:
- 树上的贪心如果过于“目光短浅”,可以使用合并连通块的方式优化决策。
- 拓扑序可以通过捆绑节点与其前驱来优化答案。
- 邻项交换真的很神奇。