1、 樹的重量
源程序名 weight.???(pas, c, cpp) 可執行文件名 weight.exe 輸入文件名 weight.in 輸出文件名 weight.out |
【問題描述】
樹可以用來表示物種之間的進化關係。一棵“進化樹”是一個帶邊權的樹,其葉節點表示一個物種,兩個葉節點之間的距離表示兩個物種的差異。現在,一個重要的問題是,根據物種之間的距離,重構相應的“進化樹”。
令N={1..n},用一個N上的矩陣M來定義樹T。其中,矩陣M滿足:對於任意的i,j,k,有M[i,j]+M[j,k]<=M[i,k]。樹T滿足:
1.葉節點屬於集合N;
2.邊權均爲非負整數;
3.dT(i,j)=M[i,j],其中dT(i,j)表示樹上i到j的最短路徑長度。
如下圖,矩陣M描述了一棵樹。
樹的重量是指樹上所有邊權之和。對於任意給出的合法矩陣M,它所能表示樹的重量是惟一確定的,不可能找到兩棵不同重量的樹,它們都符合矩陣M。你的任務就是,根據給出的矩陣M,計算M所表示樹的重量。下圖是上面給出的矩陣M所能表示的一棵樹,這棵樹的總重量爲15。
【輸入】
輸入數據包含若干組數據。每組數據的第一行是一個整數n(2<n<30)。其後n-l行,給出的是矩陣M的一個上三角(不包含對角線),矩陣中所有元素是不超過100的非負整數 。輸入數據保證合法。
輸入數據以n=0結尾。
【輸出】
對於每組輸入,輸出一行,一個整數,表示樹的重量。
【樣例】
weight.in weight.out
5 15
59 12 8 71
811 7
51
4
4
1536 60
3155
36
0
【題解】
【知識準備】
樹的基本特徵。
【算法分析】
本題是一道涉及樹性質的算法題,所以我們應該以樹的性質爲突破口,來討論本題的算法。
首先來看一下簡單的實例,就以題目中給出的例子來說明。下圖所示的樹,有5個葉子節點,兩個內點,6條邊。我們已知的信息是任意兩個葉子節點之間的距離,所以我們討論的必然是葉子節點之間的關係,不可能涉及內點。
從圖中我們可以看出,有些葉子結點是連在同一個內點上的,如①和②;也有些連在不同的內點上,如①和④。我們來看連在同一內點上的葉子節點有什麼特殊的性質。就以①和②爲例,①到③、④、⑤的距離分別爲9、12、8,②到③、④、⑤的距離分別爲8、11、7,正好都相差l。這個“1”差在哪裏呢?①連到內點的邊長爲3,②連到內點的邊長爲2,兩者相差爲1。所以,相差的“1”正好就是兩節點連到內點上的邊長的差。再看①到②的距離,由於兩葉子節點都是連在同一個內點上的,所以他們之間的距離,就是兩者到內點的邊長和。知道的邊長和以及邊長差,求出兩邊長就不難做到了。(注意:兩葉子節點連到內點的邊長是未知的)
再看一下不連在同一內點上的節點,①和④。①到②、③、⑤的距離分別爲5、9、8,④到②、③、⑤的距離分別爲11、5、4,沒有一個統一的“差”。
其實,前面的結論都是非常直觀而顯然的,只不過關鍵是如何去利用。我們先來總結一下前面的結論:
(1)如果兩葉子節點連在同一個內點上,則它們到其他葉子節點的距離有一個統一的“差”;
(2)如果兩葉子節點連在不同的內點上,則它們到其他葉子節點的距離沒有一個統一的“差”;
○
/
○────●────●
a \
○
值得注意的是,圖6-3中的a點不能算內點。我們所指的內點是至少連接兩個葉子節點的點,像a這樣的點完全可以去掉,不會影響樹的權值和,如下圖。
○
/
○─────────●
\
○
(3)如果兩葉子節點連在同一個內點上,則它們之間的距離等於它們各自到內點的邊長的和。
根據(1)和(2)兩條性質,很容易得到判斷連接相同內點的兩個葉子節點的方法,即必須滿足它們到其他所有葉子節點有統一的距離差(充分且必要)。
找到兩個連接相同內點的葉子節點並計算出它們各自到內點的邊長(不妨設爲l1和l2)以後,我們可以作這樣的操作:刪去一個節點,令另一個節點到內點的邊長爲l1+l2。這樣得到的新樹,權值和與原樹相同,但葉子節點少了一個,如下圖。
反覆利用上述操作,最後會得到一棵只有兩個葉子節點的樹,這兩個節點之間的邊長就是原樹的權值和。
算法需要反覆執行n-2次刪除節點的操作。每次操作中,需要枚舉兩個葉子節點,並且還要有一個一維的判斷,時間複雜度是O(n3)的。所以,整個算法的時間複雜度是O(n4)的。對於一個規模僅有30的題目來說,O(n4)的算法足以解決問題了。當然,算法本身應該還是有改進的餘地的,不過這裏就不加以討論了。
【代碼】
#include<cstdio>
int deleted[31];
int del,n,map[31][31];
bool check(int x,int y)
{
del=0; bool mark=true; //不能用DEL=某個數 代替MARK 會出錯
for (int a=1;a<=n;a++)
if (a!=x && a!=y && deleted[a] )
{
if (mark) { del=map[x][a]-map[y][a]; mark=false; }
else if (del!=map[x][a]-map[y][a]) return 0;
}
return 1;
}
void merge(int x,int y)
{
deleted[y]=0;
int ty=(map[x][y]-del)>>1; //計算刪去一條邊權值
for (int i=1;i<=n;i++)
if (deleted[i]) { map[x][i]+=ty; map[i][x]+=ty; } //增加距離,相當於點合成
}
void work( )
{
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
if (i!=j && deleted[i] && deleted[j] )
if (check(i,j))
{
merge(i,j);
return;
}
}
int main()
{
freopen("weight.in","r",stdin);
freopen("weight.out","w",stdout);
while ( scanf("%d",&n) && n)
{
for (int i=1;i<n;i++)
{
deleted[i]=1;
for (int j=i+1;j<=n;j++) { scanf("%d",&map[i][j]); map[j][i]=map[i][j]; }
}
deleted[n]=1;
for (int i=1;i<=n-2;i++) work(); //一共進行N-2次
int n1,n2;
for (n1=1;n1<=n;n1++) if (deleted[n1]) break;
for (n2=n1+1;n2<=n;n2++) if (deleted[n2]) break;
printf("%d\n",map[n1][n2]);
}
}