hihoCoder #1063 缩地

题目

题意

给定一棵带边权及点权的有根树 \(T(V,E)\) ( \(|V| \le 100\) , 边权 \(w\colon E \to \mathbb{N}^*\) , \(w \le 10^4\) , 点权 \(v \colon V \to \{0,1,2\}\) ). 要求回答 \(q\) ( \(q \le 10^5\) ) 组询问, 询问格式: 给定 $d \in \mathbb{N} $ ( \(d \le 10^6\) ), 问从根节点出发累计移动距离不超过 \(d\) , 经过的节点权值之和的最大值 (重复经过的节点只算一次).

树形背包

将问题向树形背包转化.
为了描述方便, 令 \(N=|V|\) , 将树的节点从 \(1\)\(N\) 编号, 且令根节点编号为 \(1\) .
DP状态:
\(dp[i][j]\) : 在子树 \(i\) 中移动, 距离不超过 \(j\) 所能获得的最大收益.
复杂度 \(O(Nd^2)\) (?) , 不能容忍.

注意到点权不超过2, 从而节点权值和为 \(O(N)\) .
考虑新DP状态:
\(dp[i][j]\) : 在子树 \(i\) 中移动, 收益 恰好\(j\) 所需的最小移动距离.
复杂度为 \(O(N^2)\) .

状态细化

上述两种DP状态是从一般背包问题出发得出的, 在本题中并不能直接转移, 需要细化 (增维); 两种DP状态的转移方式类似, 以第二种DP状态为例:
\(dp[0][i][j]\) : 在子树 \(i\) 中移动, 收益恰好为 \(j\) 且最后回到节点 \(i\) 所需的最小移动距离.
\(dp[1][i][j]\) : 在子树 \(i\) 中移动, 收益恰好为 \(j\) , 不论最后在哪个节点, 所需的最小移动距离.

转移方程

将子树看做 泛化物品 合并即可.

泛化物品

(见 崔添翼《背包九讲》)
泛化物品可一般地表示为二元组
\((n, g)\) , \(n \in \mathbb{N}\) , \(g \colon \{0, 1, 2, \dots, n\} \to \mathbb{Z}\) , \(g\) 是收益函数.
采用 启发式合并 的方法合并两泛化物品 \((n_1, g_1)\) , \((n_2, g_2)\) 的复杂度为 \(O(n_1n_2)\) .

Implementation

#include <bits/stdc++.h>
using namespace std;

const int N=105;
vector<pair<int,int>> g[N];
const int M=205;
int dp[2][N][M], v[N], tmp[M];

int dfs(int u, int f){
    int sum=v[u];
    // boundary condition
    dp[0][u][v[u]]=dp[1][u][::v[u]]=0;
    for(auto e: g[u]){
        int v=e.first, w=e.second;
        if(v!=f){
            int sum_v=dfs(v, u);
            sum+=sum_v;
            for(int i=sum; i>=::v[u]+::v[v]; --i)
                for(int j=::v[v]; j<=min(i-::v[u], sum_v); j++){
                    dp[0][u][i]=min(dp[0][u][i], dp[0][u][i-j]+dp[0][v][j]+2*w);
                    dp[1][u][i]=min(dp[1][u][i], dp[1][u][i-j]+dp[0][v][j]+2*w);
                    dp[1][u][i]=min(dp[1][u][i], dp[0][u][i-j]+dp[1][v][j]+w);
                }
        }
    }
    return sum;
}



int main(){
    int n;
    scanf("%d", &n);
    for(int i=1; i<=n; i++)
        scanf("%d", v+i);
    memset(dp, 0x3f, sizeof(dp));
    //boundary conditon
    // for(int i=1; i<=n; i++) // This is not the boundary!
    //     dp[0][i][0]=0;

    for(int i=1; i<n; i++){
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }
    int sum=dfs(1, 1);
    int q;
    scanf("%d", &q);
    for(; q--; ){
        int d;
        scanf("%d", &d);
        for(int i=sum; i>=0; i--)
            if(dp[1][1][i]<=d){
                printf("%d\n", i);
                break;
            }
    }
    return 0;
}

Reference

本文参考了解题报告1解题报告2.
对比这两篇解题报告可更好地理解:

  1. DP的基本原理与概念
  2. 泛化物品的合并

其他

关于树形背包问题的复杂度, 我感觉应该能给出一个更紧的复杂度, 仍需研究.
2015年国家集训队张恒捷的论文《DP的一些优化技巧》$\S$2.7 启发式合并DP数组 中讨论了这个问题.

将一个长为 \(x\) 的数组看做一个权重为 \(x\) 的点, 所有点权重之和为 \(m\) . 合并权重为 \(x\)\(y\) 的点需要花费 \(O(T(x,y))\) 的复杂度, 得到一个权重为 \(x+y\) 的点.

\(T(x,y) =xy\)

这种情况多数在树上问题出现. 无论按什么顺序合并, 总复杂度都是 \(O(m^2)\) 而非 \(O(m^3)\) . 关于这一点, 我们可以把合成的点向合成它的点连边, 整个结构就是一棵树, \(T(x,y)\) 相当于枚举左右子树内所有点的匹 (分?) 配方案. 这时只要注意到每一对点只会在他们 LCA 处枚举到就行了.
所以该情况的复杂度为 \(O(m^2)\) .

不难分析出这道题的复杂度为 \(O(N^2)\) .
需要特别注意的是, 这个题目不是一个典型的背包问题, 而是一个树上的合并问题. 在背包问题中, 合并两泛化物品受制于背包容量 \(C\) , 复杂度不超过 \(O(C^2)\) . 树形背包的复杂度还需要研究.

posted @ 2017-01-29 23:35  Pat  阅读(398)  评论(0编辑  收藏  举报