題目大意
給定一顆以1
爲根節點的樹,每個節點有一個購入價格p
和賣出價格q
。
進入一個節點時需要花費p
,離開時可以收回q
,每個節點只產生一次購入和賣出。
請你選擇一個遍歷的順序,要求在遍歷的過程中身上的錢數不小於0,且出發時帶的錢數最少。
按照遍歷的順序是指:當你選擇了一顆子樹之後,你需要將這個子樹全部走完,才能選擇其他子樹。
解題思路
該題爲一道樹形圖上的貪心問題。
我們每一步的決策在於:當遍歷到一個根時,對於其擁有的若干顆子樹,應該以怎樣的順序來遍歷這些子樹。
先將問題簡化爲二叉樹,並且兩顆子樹都只有根節點:
首先我們選擇左邊的路徑,則需要攜帶的錢數爲:
經過1: L1 = p1
經過2: L2 = p1-q1+p2
經過3: L3 = p1-q1+p2-q2+p3
然後考慮走右邊的情況:
經過1: R1 = p1
經過3: R2 = p1-q1+p3
經過2: R3 = p1-q1+p3-q3+p2
對於這6個值,我們要求的是min( max(L1, L2,
L3), max(R1, R2, R3) )
由於在題目中已經明確說明總是有p≥q
,可以肯定有L3≥R2,R3≥L2
,所以可以直接將L2
和R2
排除。
又因爲1是必經之路,因此在選擇左右時,實際上需要比較的只有L3
和R3
。
我們將L3
和R3
換一種形式表示:pSum
= p1+p2+p3, qSum = q1+q2+q3
,則L3=pSum-qSum+q3
,R3=pSum-qSum+q2
若L3>R3
,則有q3>q2
,此時要選擇的是R3
,即走右路;
若L3<R3
,則有q2>q3
,此時要選擇的是L3
,即走左路。
由於pSum
和qSum
的值是確定的,因此實際上我們只需要根據q2
和q3
的值就可以選擇走哪邊。
最後的結論也就是:對於二叉樹的情況,我們選擇q值大的一邊先走,一定能保證結果值最小
那麼接下來將這個結論推廣至多叉樹的情況:
對於多叉樹的情況,我們將子樹按照q值從大到小的順序,一定能保證結果值最小
這是由於q值之間的大小順序是絕對的,假設3個子節點分別爲A
,B
,C
,若qA≥qB
,qB≥qC
,則一定有qA≥qC
。
不妨舉個例子:
根據q值,對應的6種遍歷順序分別爲:
1,2,3:需要帶錢8
1,3,2:需要帶錢7
2,1,3:需要帶錢8
2,3,1:需要帶錢7
3,1,2:需要帶錢7
3,2,1:需要帶錢6
那麼還有一個問題,一個節點的p
和q
是直接給定的,我們應該如何來求一顆子樹的pt
和qt
(爲了便於區分,我們將樹的p
和q
記爲pt
和qt
)?
在計算過程中,我們採用遞歸的從根節點開始遍歷。在計算當前節點時,我們需要將其子樹的p
和q
都先計算出來。
因此在計算當前子樹時,我們已經知道了根節點的p
和q
,以及每顆子樹的pt
和qt
(記作pt1,pt2,pt3,...
和qt1,qt2,qt3,...
,並且滿足qt1≥qt2≥qt3≥...
)。
在該題中所有節點的總購買錢數不會超過200,000,000,不妨設置一個初始錢數wallet
= 200000000
。
同時我們還需要一個值minWallet
,來記錄錢包錢數最少時的值,初始也爲200,000,000。
首先處理根節點:
wallet = wallet - p
If (minWallet > wallet) Then
minWallet = wallet
End If
wallet = wallet + q
接下來處理每個子樹:
For i = 1 .. chdNumber
wallet = wallet - pt[i]
If (minWallet > wallet) Then
minWallet = wallet
End If
wallet = wallet + qt[i]
End For
最後可以得到整個樹的p
和q
分別爲:
pt[root] = 200000000 - minWallet // 遍歷這顆樹至少需要的錢數
qt[root] = wallet - minWallet // 最後的錢數-花費的錢數=返還的錢數
完整的僞代碼爲:
DFS(rt):
wallet = 200000000
minWallet = 200000000
// 先遞歸處理每個子樹
For each child i of rt
DFS(i)
End For
// 對該根節點的所有子樹按照qt值排序
sortByQt(children)
// 處理根節點
wallet = wallet - p[rt]
If (minWallet > wallet) Then
minWallet = wallet
End If
wallet = wallet + q[rt]
// 處理子樹
For each child i of rt
wallet = wallet - pt[i]
If (minWallet > wallet) Then
minWallet = wallet
End If
wallet = wallet + qt[i]
End For
// 記錄子樹
pt[rt] = 200000000 - minWallet
qt[rt] = wallet - minWallet
最後所求的結果是pt[1]
。
由於在每一顆子樹都使用了排序,最後這個算法的時間複雜度爲O(NlogN),即使在N
爲10000的數據下,也能夠順理通過。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 10005;
const int inf = 200000000;
struct Node
{
vector<int> child;
}tree[maxn];
int n,p[maxn],q[maxn];
int pt[maxn],qt[maxn];
bool cmp(int a,int b)
{
return qt[a] > qt[b];
}
void dfs(int u,int fa)
{
int Wallet = inf;
int minWallet = inf;
vector<int> V = tree[u].child;
for(int i = 0; i < V.size(); i++)
{
int v = V[i];
if(v == fa) continue;
dfs(v,u);
}
sort(V.begin(),V.end(),cmp);
Wallet = Wallet - p[u];
if(minWallet > Wallet)
minWallet = Wallet;
Wallet = Wallet + q[u];
for(int i = 0; i < V.size(); i++)
{
if(V[i] == fa) continue;
Wallet = Wallet - pt[V[i]];
if(minWallet > Wallet)
minWallet = Wallet;
Wallet = Wallet + qt[V[i]];
}
pt[u] = inf - minWallet;
qt[u] = Wallet - minWallet;
}
int main()
{
int u,v;
while(scanf("%d",&n)!=EOF)
{
for(int i = 1; i <= n; i++)
scanf("%d%d",&p[i],&q[i]);
for(int i = 1; i <= n; i++) tree[i].child.clear();
for(int i = 1; i < n; i++)
{
scanf("%d%d",&u,&v);
tree[u].child.push_back(v);
tree[v].child.push_back(u);
}
dfs(1,0);
printf("%d\n",pt[1]);
}
return 0;
}