1. 程式人生 > >洛谷 P1273 有線電視網(樹形揹包)

洛谷 P1273 有線電視網(樹形揹包)

洛谷 P1273 有線電視網(樹形揹包)

乾透一道題

題面:洛谷 P1273

本質就是個揹包。這道題dp有點奇怪,最終答案並不是dp值,而是最後遍歷尋找那個合法且最優的\(i\)作為答案。dp值存的是當前狀態下的成本,所以合法情況即當成本值大於等於0,不虧本的時候。

因為dp維護的是成本,並且按照揹包思想,存在讓這個使用者接入和不讓這個使用者接入兩種決策,類比揹包,所以狀態轉移方程容易得到原始方程:
\[ dp[s][i][j]=max \{ dp[s][i-1][j-k]+dp[w][size_w][k]-cost[s][w](w \in son[s]) \} \]
\(dp[s][i][j]\)

表示當前節點\(s\)的前\(i\)個兒子中選取\(j\)個使用者的成本,決策當前兒子接入\(k\)個使用者。但是這裡我們可以像01揹包那樣優化一維\(i\),只要我們遞增遍歷\(i\),遞減遍歷\(j\)即可,因為這樣\(i-1\)輪的\(dp[s][i-1][j-k]\)狀態才沒有被第\(i\)輪的\(dp[s][i][j-k]\)狀態覆蓋,並且\(dp[w][k]\)最終的值就是\(dp[w][size_w][k]\)

最終二維的dp方程:
\[ dp[s][j]=max \{ dp[s][j-k]+dp[w][k]-cost[s][w](w \in son[s]) \} \]
192ms AC Code:

#include <cstdio>
using namespace std;
#define MAXN 3003
#define MAX(A,B) ((A)>(B)?(A):(B))
#define INF 0x3ffffff
struct e{
    int w,v,nxt;
} edge[10000010]; //邊數一定要開大!
int dp[MAXN][MAXN],head[MAXN],sz[MAXN];
int n,m,cnt_e;
inline void adde(int u, int v, int w){
    edge[++cnt_e].w=w;
    edge[cnt_e].v=v;
    edge[cnt_e].nxt=head[u];
    head[u]=cnt_e;
}
int solve(int x, int fa){
    if(x>=n-m+1&&x<=n) //是否為使用者端
        return sz[x]=1;
    for(register int i=head[x];i;i=edge[i].nxt){ //”遞增”遍歷兒子i
        sz[x]+=solve(edge[i].v, x); //樹形dp通常自下而上更新
        for(register int j=sz[x];j>=0;--j) //列舉狀態
            for(register int k=0;k<=sz[edge[i].v];++k)  //列舉決策,當前兒子取幾個使用者
                dp[x][j]=MAX(dp[x][j], dp[x][j-k]+dp[edge[i].v][k]-edge[i].w);
        //dp[s][i][j]=max{dp[s][i-1][j-k]+dp[w][size_w][k]-cost[s][w]} 原始狀態轉移方程
    }
    return sz[x];
}
int main() {
    scanf("%d %d", &n, &m);
    for(register int i=0;i<=n;i++)
        for(register int j=0;j<=m;j++)
            dp[i][j]=-INF;
    for (int i=1;i<=n;i++) dp[i][0]=0; //注意初始化!
    for(int i=1;i<=n-m;++i){
        int sz;
        scanf("%d", &sz);
        for(int j=1;j<=sz;++j){
            int a,c;
            scanf("%d %d", &a, &c);
            adde(i, a, c); //鏈式前向星建邊
        }
    }
    for(int i=n-m+1;i<=n;++i)
        scanf("%d", &dp[i][1]);
    solve(1,0);
    for(register int i=m;i>=0;--i) //遞減遍歷找到第一個合法值
        if(dp[1][i]>=0){
            printf("%d\n", i);
            break;
        }
    return 0;
}

需要注意:

  • 鏈式前向星邊數不是\(N\),一定要開大
  • dp的遍歷順序一定要正確,因為你已經優化了一維\(i\)
  • 使用者端支付的錢\(w\)應該表示為\(dp[n][1]=w\)
  • dp一定要結合dp方程初始化