1. 程式人生 > >綠色計算大賽決賽 第二階段 消息傳遞(斯坦納樹 狀壓dp+spfa)

綠色計算大賽決賽 第二階段 消息傳遞(斯坦納樹 狀壓dp+spfa)

com end clu $$ 如果 void ati ron ***

傳送門

Description 作為公司老板的你手下有N個員工,其中有M個特殊員工。現在,你有一個消息需要傳遞給你的特殊員工。因為你的公司業務非常緊張,所以你和員工之間以及員工之間傳遞消息會造成損失。因此,你希望只告訴一部分特殊員工,然後依靠員工之間傳遞消息,使得所有的特殊員工都能獲得要傳遞的消息,同時使得損失最小。同時,你不關心要傳遞的消息是否經過了其它員工。求最小的損失。 Constraint 補全右側代碼區中的int solve(int N, vector cost_e, vector employees, vector cost_b)函數,完成挑戰任務中提出的要求:返回最小的損失。
如果需要,你可以在solve函數外添加其它代碼,但是不要改變Solver類的名字以及solve函數的形式,也不要改變DeliveryCost類的定義。
函數參數說明如下:
  • int N:員工個數(2 <= N <= 50),員工編號從1到N;
  • vector<DeliveryCost> cost_e:員工之間傳遞消息的損失,員工cost_e[i].u和cost_e[i].v之間傳遞消息的損失為cost_e[i].cost。數據保證任意兩個員工之間傳遞消息的損失只出現一次,整個數組長度為N(N-1)/2。(1 <= cost <= 1000)
  • vector<int> employees:特殊員工的編號,個數為M(1 <= M <= 10);
  • vector<int> cost_b:你傳遞給每個特殊員工的損失,與employees一一對應。(1 <= cost <= 1000)
Input N = 3;
cost_e = {{1, 2, 2}, {1, 3, 2}, {2, 3, 2}};
employees = {1, 2};
cost_b = {1, 1000}; Output 3 題意 給出n個點的完全圖,另外還有一個點,向其中的m個點連有向邊。求至少包含這個點和m個點的最小連通圖,並輸出最小的邊權和。 分析 首先,問題是求最小邊集,邊的有向無向其實不重要,所以從單獨的點連向m個點的那些有向邊,可以直接看成無向邊,因此單獨的點和那m個點是完全相同的,不用再單獨考慮;
問題轉化為:已知N個點M條無向帶權邊,求一個最小連通圖,必須包含其中的K個特殊點。因為要邊權的花費最小,所以圖中是不應該出現環的,最小連通圖一定是一棵樹。具有這樣性質的樹,被定義為斯坦納樹。
斯坦納樹的求解貌似是一個NP問題,做法基本還是暴力,但是因為這道題數據量很小,所以是可做的。推薦一篇大佬分析斯坦納樹的博客。
因為特殊點最多只有11個,所以暴力搜可以從K入手,先大致生成K個點的一個生成樹,再看能不能借用其他N-K個點形成的網絡的一部分來降低花費。
具體地來說,把狀態定義為$$$(i, (a_1a_2...a_k)_{bin})$$$,表示以$$$i$$$號節點為根的一棵樹,$$$a_j$$$為1則表示$$$i$$$至少與第$$$j$$$個特殊點是連通的。
現在用$$$state$$$簡記$$$(a_1a_2...a_k)_{bin}$$$,那麽$$$dp[i][state]$$$記錄狀態$$$(i,state)$$$的最小花費,有下面兩個轉移方程:
$$$$$$ \begin{align} & dp[i][state]=min_{substate\subset state}\{dp[i][substate]+dp[i][state-substate]\} \ & dp[i][state]=min\{dp[i][state],dp[j][state]+w(i,j)\} \end{align} $$$$$$ 對這塊理解不是很到位,以下可能有不嚴謹之處。
第一層轉移,對於固定的$$$i$$$,大的$$$state$$$的$$$dp$$$用$$$substate$$$的dp求出並取最小,相當於用兩棵小的樹可以合並成一棵大一點的樹。
然後第二層轉移,所有的生成樹大致長什麽樣都知道了,但很可能還不是最優的,還要進一步減少花費。轉移方程的形式其實很像求最短路的形式,對於固定的$$$state$$$,相當於把它們合並為一個新的點,並且就以這個點為起點,在新的圖上跑一次最短路,也就是借用其他的點$$$j$$$對原來的邊進行了松弛。
搜索完所有的狀態以後,任意一個特殊點$$$X$$$,則$$$dp[X][{1...1}]$$$就是我們要求的斯坦納樹的最小花費。
比賽的時候想到的錯誤算法:
  • 網絡流:
    • 從單獨的點出發->特殊點, 特殊點<->其他點, 特殊點->匯點)。
    • 錯誤原因:正解的流出量可以大於流入量,導致費用大的邊也被選擇,或者一條邊對答案貢獻多次。
  • 縮邊
    • 找K個特殊點,兩兩之間的最短路,縮為一條邊,構造新的圖,求K個點的最小生成樹
    • 錯誤原因:特殊點之間的最短路可以分叉,縮邊會對分叉前的公共部分重復計算。

代碼
#include <stdio.h>
#include<queue>
#include<vector>
#include <memory.h>
using std::queue;
using std::vector;
/*
 *dp:
 *dp[i][st] 包含第i個點,且至少和state為1的關鍵點相連的最小花費
 *轉移
 *dp[i][st]=Min{dp[i][st],dp[i][st-sub]+dp[i][sub]} 分解為兩個
 *dp[i][st]=Min{dp[i][st],dp[j][st]+w(i,j)} i和j有邊,關鍵點外面的部分spfa一下
 */
#define INF 0x3f3f3f3f
#define maxn 55
int g[maxn][maxn];
int dp[maxn][1 << 12];
queue<int> help;
int N,K;
int vis[maxn];
struct DeliveryCost {
    int u;
    int v;
    int cost;
};
void spfa(int cs){
    while(!help.empty())    {
        int id = help.front();help.pop();
        vis[id] = 0;
        for(int i=1;i<=N;++i){
            if (id == i || g[id][i] == INF)continue;
            if(dp[i][cs]>dp[id][cs]+g[id][i]){
                dp[i][cs] = dp[id][cs] + g[id][i];
                if(!vis[i]){
                    vis[i] = 1; help.push(i);
                }
            }
        }
    }
}

int solve(int n,
    vector<DeliveryCost> cost_e,
    vector<int> employees,
    vector<int> cost_b) {
    /*********begin*********/
    memset(g, 0x3f, sizeof g);
    memset(dp, 0x3f, sizeof dp);
    //建圖
    //員工到員工
    int sz = cost_e.size();
    int tu, tv, tc;
    for(int i=0;i<sz;i++)    {
        tu = cost_e[i].u; tv = cost_e[i].v; tc = cost_e[i].cost;
        g[tu][tv] = tc;
        g[tv][tu] = tc;
    }
    //老板到特殊員工
    K = cost_b.size();
    for(int i=0;i<K;i++)    {
        g[n + 1][employees[i]] = cost_b[i];
        g[employees[i]][n+1] = cost_b[i];
        dp[employees[i]][1 << i] = 0;
    }
    dp[n + 1][1 << K] = 0;
    K++; N=n+1;
    int limit = (1 << K) - 1;
    //第一層轉移
    for(int sta=0;sta<=limit;sta++)    {//遍歷state
        for(int i=1;i<=N;i++)    {
            for(int s=sta;s;s=(s-1)&sta)    //遍歷substate
                if(dp[i][s]+dp[i][sta-s]<dp[i][sta])
                    dp[i][sta] = dp[i][s] + dp[i][sta - s];
            if (dp[i][sta] < INF)    {//i-sta被松弛,放入隊列
                help.push(i);
                vis[i] = 1;
            }
        }
        //第二層轉移
        spfa(sta);
    }
    //N是特殊點中的一個
    return dp[N][limit];
    /*********end*********/
    
}
int main(){
    //測試一下樣例
    int n = 3;
    vector<DeliveryCost>cost_e{ { 1, 2, 2 },{ 1, 3, 2 },{ 2, 3, 2 } };
    vector<int> employees{ 1,2 };
    vector<int> cost_b{ 1,1000 };
    printf("%d",solve(n, cost_e, employees, cost_b));
}
總結 雖然過了,還是來算一下復雜度吧
以每個點為根都有$$$2^k$$$個$$$state$$$,求解每一個都遍歷了其所有$$$substate$$$。含$$$x$$$個$$$1$$$的state共$$$C_k^x$$$個,$$$substate$$$數量都是$$$2^x$$$,所以復雜度為
$$$$$$\begin{align} & n\cdot\sum{C_k^x\cdot 2^x}\ =& n\cdot\sum{C_k^x\cdot 1^{k-x}\cdot 2^x}\ =& (1+2)^kn =3^kn \end{align}$$$$$$ 此外,對每個$$$state$$$,都要跑一遍$$$spfa$$$,因為是稠密圖,如果按spfa的最壞情況來看就是:
$$$$$$\begin{align} & 2^k\cdot O(spfa)\ =& 2^k\cdot O(VE)\ =& 2^k\cdot O(n\cdot \frac{n(n-1)}{2})\ =& 2^k\cdot O(n^3)\ \end{align}$$$$$$ 最終復雜度為$$$O(3^kn+2^kn^3)=O(2^kn^3)$$$,大約在3e8左右,但是實際上很快,十組樣例只跑了14.408秒,可能是因為$$$spfa$$$的復雜度只有$$$O(kE)$$$吧,也有可能是因為數據比較水233。

綠色計算大賽決賽 第二階段 消息傳遞(斯坦納樹 狀壓dp+spfa)