1. 程式人生 > >藍橋杯 演算法提高-金屬採集

藍橋杯 演算法提高-金屬採集

金屬採礦

人類在火星上發現了一種新的金屬!這些金屬分佈在一些奇怪的地方,不妨叫它節點好了。一些節點之間有道路相連,所有的節點和道路形成了一棵樹。一共有 n 個節點,這些節點被編號為 1~n 。人類將 k 個機器人送上了火星,目的是採集這些金屬。這些機器人都被送到了一個指定的著落點, S 號節點。每個機器人在著落之後,必須沿著道路行走。當機器人到達一個節點時,它會採集這個節點蘊藏的所有金屬礦。當機器人完成自己的任務之後,可以從任意一個節點返回地球。當然,回到地球的機器人就無法再到火星去了。我們已經提前測量出了每條道路的資訊,包括它的兩個端點 x 和 y,以及通過這條道路需要花費的能量 w 。我們想花費盡量少的能量採集所有節點的金屬,這個任務就交給你了。

解題思路

整個樹中任意邊都是雙向的,所以雖然題目給定的是S點為初始點,仍可以把S點看成樹的root,有了這個事實,題目就是轉化為一個典型的樹狀dp。
首先要思考一個問題,如何定義狀態和狀態轉移方程。
按照題意,對於任一頂點,設在該點有k個機器人,求出遍歷它的全部子節點花費的最小代價。那麼對於一個子樹,我們可以給它[1,k]個機器人讓他們遍歷子樹,然後讓他們停在某些位置,當然也可以讓這些機器人遍歷完返回。
這裡考慮一個問題,對一棵子樹使用x個機器人進行遍歷(x>1),且x個機器人全部返回,最小代價一定比用1個機器人進行遍歷整個子樹且返回大。同樣,如果對一棵子樹部分機器人返回,部分不返回,則這種情況一定沒有僅僅只用不返回的部分遍歷整棵子樹且不返回小。所以這兩種情況都不需要考慮,後面引入分組揹包之後會再討論。其實這也恰恰說明了動態規劃必須對每一種狀態的轉移進行全面的考慮,不能漏掉任何一種決策,但對於某一些一定不會選擇的決策,動態規劃就無需考慮這些決策。學習動態規劃這些天,我最大的感受就是列舉回朔是一個盲打莽撞的小孩子,而動態規劃則是一個精於算計的大人,每一步都是有目的的狀態轉移。我理解的還是太淺了,希望通過不斷的做題學習能夠對動態規劃有更深入的理解。

好了,問題已經變成對於一個根節點給定k個機器人,把這些機器人全部或部分分配給子節點,遍歷全部節點所需要的最小代價。
這裡意圖已經很明顯了,對於根節點的每一個子樹都可以使用[1,k]個機器人,而根節點就是通過選擇一種分配策略,使得代價最小。不難想到這就是一個分組揹包問題。
所謂分組揹包,就是有n組物品,每一個物品都有自己的重量和價值,從每一個揹包裡選擇至多一個物品,使得揹包在不超過最大重量限制的時候價值最大。而在這道題裡,每一個子樹就是一個分組,[1,k]個機器人就是物品的重量,給子樹x個機器人所耗費的最小代價就是他們的價值。所不同的是:1.對於每一個子樹,按照上面的討論還需要考慮派給一個機器人是的最小代價。2.每一個子樹一定需要被遍歷,所以每一個分組一定需要被遍歷,這很簡單,只要在程式碼里加上一點特殊處理即可。
有了分組揹包的思想,再去考慮上文說到的n個機器人全部返回(這裡的返回不是題意裡的返回地球,而是返回到當前遍歷的根節點)的問題,這其實就相當於這個分組裡的一個重量為0(因為若選擇了這個方案,機器人並沒有損耗),耗費的能量卻比用一個機器人遍歷整個子樹來的多。那麼在決策的時候,他一定不會被選(很簡單,因為他和1個機器人遍歷用的能量還多,但其他分組選擇的方案用的能量卻一樣,相比之下一定不會選擇它)。舉一反三,可以知道n個機器人返回a個,n-a個不返回的問題相當於重量為n-a個的物品,它對其他分組的決策產生的影響和用n-a個機器人遍歷且不返回是一樣的,但耗費的能量更大。
這樣考慮的話,我們其實不需要考慮這些機器人返回的可能了,而僅僅保留一個機器人遍歷且返回的情況。

下面給出狀態和狀態轉移方程,需要注意的是,由於這個問題的最優解是子節點所有機器人數目下的最優解的組合,狀態定義不僅要考慮該節點給定k個機器人時的最小能量,還需要引入一個附加分量,這個附加分量是為了通過分組揹包的思想求得最後的最優解而設立的,用來標記給前n個子節點分配機器人時能獲得的最小能量。而在程式裡省略了這一分量,這是可行的,這是常用的一種技巧,滾動陣列。
定義如下

d(r,n,k) 為對節點r的前n個子節點分配k個機器人獲得的最小能量
d(r,n,k)=min{d(n,C(n),ki)+d(r,n1,kki)+W(r,n)(ki!=0?ki:2)|ki=0,1,2...,k}
其中W(r,n)為邊rn的權重,當k為0時,機器人會往返,所以應該為權重的兩倍。

下面貼出程式碼

#include <iostream>
#include <cstring>

using namespace std;

#define N 100001

struct edge
{
    int to;
    int weight;
    int next;
};

int tree[N];

edge edges[2*N];

int d[N][12];

int K;

int ep = 0;
void add_edge(int x, int y, int w)
{
    edges[ep].to = y;
    edges[ep].weight = w;
    edges[ep].next = tree[x];
    tree[x] = ep++;
}

void dfs(int r, int p)
{
    for(int cur = tree[r]; cur != -1; cur=edges[cur].next)
    {
        int child = edges[cur].to;
        if(child == p)
            continue;
        dfs(child, r);
        for(int i = K; i >= 0; i--)      // d[r][i] = ?
        {
            d[r][i] += d[child][0]+edges[cur].weight*2;

            for(int j = 1; j <= i; j++)
            {
                if(d[r][i] > d[child][j] + d[r][i-j] + j*edges[cur].weight)
                {
                    d[r][i] = d[child][j] + d[r][i-j] + j*edges[cur].weight;
                }
            }
        }

    }
}

int main()
{
    memset(tree, -1, sizeof(tree));
    int n, S;
    std::cin >> n >> S >> K;
    for(int i = 0; i < n-1; i++)
    {
        int a, b ,c;
        std::cin >> a >> b >> c;
        add_edge(a, b, c);
        add_edge(b, a, c);
    }

    dfs(S, -1);
    std::cout << d[S][K] << std::endl;
}