【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
那我們就可以對於每個子樹都看它怎麼排列好。我們貪心一下。
首先,對於兩個子樹
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
那假設
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。
那這個我們可以用堆來維護。
但是這是不能直接遞迴來搞的,我們要把每個點都看成獨立,然後想父親的方向合併。
那顯然上面的貪心在這裡還是可以的。
那我們要維護 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;
}