1. 程式人生 > >樹形揹包 POJ1155TELE

樹形揹包 POJ1155TELE

題目描述

某收費有線電視網計劃轉播一場重要的足球比賽。他們的轉播網和使用者終端構成一棵樹狀結構,這棵樹的根結點位於足球比賽的現場,樹葉為各個使用者終端,其他中轉站為該樹的內部節點。

從轉播站到轉播站以及從轉播站到所有使用者終端的訊號傳輸費用都是已知的,一場轉播的總費用等於傳輸訊號的費用總和。

現在每個使用者都準備了一筆費用想觀看這場精彩的足球比賽,有線電視網有權決定給哪些使用者提供訊號而不給哪些使用者提供訊號。

寫一個程式找出一個方案使得有線電視網在不虧本的情況下使觀看轉播的使用者儘可能多。

輸入輸出格式

輸入格式:

輸入檔案的第一行包含兩個用空格隔開的整數N和M,其中2≤N≤3000,1≤M≤N-1,N為整個有線電視網的結點總數,M為使用者終端的數量。

第一個轉播站即樹的根結點編號為1,其他的轉播站編號為2到N-M,使用者終端編號為N-M+1到N。

接下來的N-M行每行表示—個轉播站的資料,第i+1行表示第i個轉播站的資料,其格式如下:

K A1 C1 A2 C2 … Ak Ck

K表示該轉播站下接K個結點(轉播站或使用者),每個結點對應一對整數A與C,A表示結點編號,C表示從當前轉播站傳輸訊號到結點A的費用。最後一行依次表示所有使用者為觀看比賽而準備支付的錢數。

輸出格式:

輸出檔案僅一行,包含一個整數,表示上述問題所要求的最大使用者數。

輸入輸出樣例

輸入樣例#1: 複製

5 3
2 2 2 5 3
2 3 2 4 3
3 4 2

輸出樣例#1: 複製

2

說明

樣例解釋

如圖所示,共有五個結點。結點①為根結點,即現場直播站,②為一箇中轉站,③④⑤為使用者端,共M個,編號從N-M+1到N,他們為觀看比賽分別準備的錢數為3、4、2,從結點①可以傳送訊號到結點②,費用為2,也可以傳送訊號到結點⑤,費用為3(第二行資料所示),從結點②可以傳輸訊號到結點③,費用為2。也可傳輸訊號到結點④,費用為3(第三行資料所示),如果要讓所有使用者(③④⑤)都能看上比賽,則訊號傳輸的總費用為:

2+3+2+3=10,大於使用者願意支付的總費用3+4+2=9,有線電視網就虧本了,而只讓③④兩個使用者看比賽就不虧本了。

演算法分析:

樹形揹包DP的典型例題

明確dp[i][j]含義,表示i節點,選j個使用者,能得到的錢的最大值,然後對每個節點做分組揹包。

首先,揹包的總容量相當於該點為根節點的子樹中所有的使用者數量(dp[i][j]的 j 不可能超過它連線的所有使用者數)。然後,把該節點的每個兒子看成一組,每組中的元素為選一個,選兩個...選n個使用者。

轉移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-這條邊的花費) i,j不解釋了,v表示列舉到這一組(即i的兒子),k表示列舉到這組中的元素:選k個使用者。

最後輸出dp[1][i]>=0的i的最大值,所以反向列舉。

簡單來說就是把每個節點看成一個揹包啦,它的容量就是以這個節點為根的子樹大小,組數就是連線的兒子個數。

每組都有很多選擇,選一個,兩個,三個使用者,把這些選擇當做組中的元素就好了,容易看出每組中只能選一個元素,比如你選擇了選一個使用者,就不可能同時選擇選兩個使用者。

程式碼實現:

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <stack>
#include <vector>
#include <queue>
#include <set>
#include <map>
#include <string>
#include <math.h>
#include <stdlib.h>
#include <time.h>
using namespace std;
const int N=3010;
struct node
{
	int v;///終端點
    int next;///下一條同樣起點的邊號
    int w;///權值
}edge[N*2];///無向邊,2倍
int head[N];///head[u]=i表示以u為起點的所有邊中的第一條邊是 i號邊
int tot;  ///總邊數
int minn;
int v[N],num[N];
void add(int u,int v,int w)
{
	edge[tot].v=v;
	edge[tot].w=w;
    edge[tot].next=head[u];
    head[u]=tot++;
}
int n,m;
int dp[N][N];
void dfs(int u,int fa) 
{
	
	//int num=0;     ///記錄u的節點數,揹包容量
	dp[u][0]=0;
	if(u>n-m)
	{
		dp[u][1]=v[u];
		num[u]=1;
		return ;
	}      
	for(int i=head[u];i!=-1;i=edge[i].next)
	 {
	 	int v= edge[i].v;
	    if(fa==v) continue;   ///如果下一個相鄰節點就是父節點,則證明到底層了,開始遞迴父節點的兄弟節點
		
		dfs(v,u);
		num[u]+=num[v];
		for(int j=num[u];j>0;j--)    ///揹包容量
			for(int k=1;k<=num[v];k++)   ///選擇使用者
		{
		    dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-edge[i].w);
		}
	 }

}
int main()
{
    scanf("%d%d",&n,&m) ;
	memset(head,-1,sizeof(head));
	memset(num,0,sizeof(num));
    memset(dp,~0x3f,sizeof(dp)); 
  	tot=0;
  	
  	for(int i=1;i<=n-m;i++)
	{
		
		int w,v,num;
		scanf("%d",&num);
		for(int j=1;j<=num;j++)
		{
		scanf("%d%d",&v,&w);
		add(i,v,w);	
		add(v,i,w);	
		}	
	}
	for(int i=n-m+1;i<=n;i++)
		scanf("%d",&v[i]);
  	dfs(1,-1);	
    for (int i=n;i>=1;i--)
	    if (dp[1][i]>=0)
        {
            printf("%d",i);
            break;
        } 
  	
    return 0;
}