1. 程式人生 > 其它 >【luogu AT3957】[AGC023F] 01 on Tree

【luogu AT3957】[AGC023F] 01 on Tree

技術標籤:# 並查集# 貪心# 樹並查集貪心演算法逆序對

01 on Tree

題目連結:luogu AT3957

題目大意

有一棵根為 1 1 1 的樹,每個節點有個值 0 0 0 1 1 1
然後每次你可以把一個沒有父親的點刪除,然後把值放進一個數組裡。
要你得出的陣列逆序對儘可能少,要輸出這個最小的逆序對個數。

思路

那我們會發現從根節點開始刪會很麻煩,很難處理,那我們考慮反著來:從葉節點開始不斷合併,向根節點上傳答案。

那我們要先發現一件事,對於一個點 x x x 的一個子樹 y y y,它不管 y y y 裡面怎麼排列,裡面產生了多少個逆序對,最終排列 x x

x 裡面的每個子樹的時候,看的只是 y y y 裡面有多少個 0 0 0,多少個 1 1 1,是不會管裡面怎麼排列的。

那我們就可以對於每個子樹都看它怎麼排列好。我們貪心一下。
首先,對於兩個子樹 i , j i,j i,j,設它們 0 0 0 的數量為 n u m 0 i , n u m 0 j num0_i,num0_j num0i,num0j 1 1 1 的數量為 n u m 1 i , n u m 1 j num1_i,num1_j num1i,num1j
那如果 i i i j j j 的前面,新增逆序對的個數就是 n u m 1 i × n u m 0 j num1_i\times num0_j

num1i×num0j。如果在後面,就是 n u m 1 j × n u m 0 i num1_j\times num0_i num1j×num0i
那假設 i i i 放前面比 j j j 放前面優,那就是 n u m 1 i × n u m 0 j < n u m 1 j × n u m 0 i num1_i\times num0_j < num1_j\times num0_i num1i×num0j<num1j×num0i

那這個我們可以用堆來維護。

但是這是不能直接遞迴來搞的,我們要把每個點都看成獨立,然後想父親的方向合併。

那其實 n u m 0 , n u m 1 num0,num1 num0,num1 記錄的其實變成了這個點所在的連通塊的 0 , 1 0,1 0,1 個數。
那顯然上面的貪心在這裡還是可以的。

那我們要維護 0 , 1 0,1 0,1 個數,自然要用並查集。

記得要判斷當前點是否被刪掉,因為當合並完之後,它父親節點要刪去,我們只要看 n u m 0 , n u m 1 num0,num1 num0,num1,就可以得知是否被合併。

還有一點就是 1 1 1,也就是根節點是不用再合併的,因為沒有父親。

程式碼

#include<queue>
#include<cstdio>

using namespace std;

struct Teap {
	int x, num_1, num_0;
};
bool operator < (Teap x, Teap y) {//用堆將點按貪心思想排序
	return 1ll * x.num_0 * y.num_1 < 1ll * x.num_1 * y.num_0;
}

int n, a[200001], father[200001];
int fa[200001], num[200001][2];
long long ans;
priority_queue <Teap> q;

int find(int now) {//並查集
	if (father[now] == now) return now;
	return father[now] = find(father[now]);
}

int main() {
	scanf("%d", &n);
	for (int i = 2; i <= n; i++) {
		scanf("%d", &fa[i]);
	}
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		num[i][a[i]]++;
		
		father[i] = i;
	}
	
	for (int i = 2; i <= n; i++)
		q.push((Teap){i, num[i][1], num[i][0]});
	
	while (!q.empty()) {
		Teap now = q.top();
		q.pop();
		
		int x = find(now.x);
		if (num[x][0] != now.num_0 || num[x][1] != now.num_1)
			continue;//這個點已近被刪除
		
		int y = find(fa[x]);
		ans += 1ll * num[x][0] * num[y][1];//加上逆序對個數
		
		num[y][0] += num[x][0];//這個子樹所包含的0/1的個數增加
		num[y][1] += num[x][1];
		
		father[x] = y;//並查集連線
		
		if (y != 1)//繼續下去
			q.push((Teap){y, num[y][1], num[y][0]});
	}
	
	printf("%lld", ans);
	
	return 0;
}