【树DP】BZOJ3836[Poi2014]Tourism

Description
给定一个 n 个点,m条边的无向图,其中你在第 i 个点建立旅游站点的费用为Ci。在这张图中,任意两点间不存在节点数超过 10 的简单路径。请找到一种费用最小的建立旅游站点的方案,使得每个点要么建立了旅游站点,要么与它有边直接相连的点里至少有一个点建立了旅游站点。
Input
第一行包含两个正整数 n,m(1<=n<=20000,0<=m<=25000) ,分别表示点数和边数。
第二行包含 n 个整数,其中第i个数为 Ci(0<=Ci<=10000) ,表示在第 i 个点建立旅游站点的费用。
接下来m行,每行两个正整数 u,v(1<=u,v<=n) ,表示 u v之间连了一条边,保证没有重边
Output
输出一行一个整数,即最小的总费用。
Sample Input
6 6
3 8 5 6 2 2
1 2
2 3
1 3
3 4
4 5
4 6
Sample Output
7
HINT
分别在156号站点建立旅游站点。

据说没有任意两点间不存在节点数超过 10 的简单路径就是一个NP问题?
为了做DP,就根据dfs搜索树进行树DP。
然后发现对一个点的影响不仅有它的儿子,可能还有它的祖先……
然后就一脸懵逼辣QAQ

对每一个独立的联通块DP,令 f[i][s] 表示在dfs搜索树中深度为 i 的点,其祖先的状态为s。其中 s 是一个三进制数。对于s中每一位上, 0 表示该点选了,1表示该点未选且这个点不满足要求, 2 表示该点未选但这个点满足要求(即它的邻接点选了)。

如何转移?
访问节点u,其深度为 dep 时,先找出它的邻接点且为它的祖先。
第一种转移:节点 u 不选。
若其中有一个点选了,则f[dep][s+2pow[dep]]=min(f[dep][s+2pow[dep]],f[dep1][s]);否则 f[dep][s+pow[dep]]=min(f[dep][s+pow[dep]],f[dep1][s]+val[u])
第二种转移:节点 u 选了。
那么将s中为 1 的点改成2即可。

对于它的儿子,直接用 min(f[dep+1][s],f[dep+1][s+2pow[dep+1]]) 来更新 f[dep][s] 即可。

这道题与其他的树DP不同的地方在于要考虑祖先的影响……

#include <iostream>
#include <cstdio>
#include <cstring>
#define MAXN 20005
#define MAXM 25005
using namespace std;
const int inf=1e8;

int n, m, val[MAXN], ans;
int power[15], f[15][59060], d[MAXN], tmp[MAXN];
bool vis[MAXN];

struct node
{
    int v, next;
}edge[MAXM<<1];
int adj[MAXN], pos;

void add(int a,int b)
{
    edge[pos].v=b, edge[pos].next=adj[a];
    adj[a]=pos;++pos;
}

void dfs(int u)
{
    vis[u]=1;
    int v, dep=d[u];
    if(!dep)f[0][0]=val[u], f[0][1]=0, f[0][2]=inf;
    else
    {
        int cnt=0;
        for(int p=adj[u];~p;p=edge[p].next)
            if(vis[(v=edge[p].v)]&&d[v]<dep)
                tmp[++cnt]=d[v];
        for(int s=0;s<power[dep+1];++s)f[dep][s]=inf;
        for(int s=0, t, l;s<power[dep];++s)
        {
            t=1, l=s;
            for(int i=1;i<=cnt;++i)
                if(s/power[tmp[i]]%3==0)t=2;
                else if(s/power[tmp[i]]%3==1)l+=power[tmp[i]];
            f[dep][s+t*power[dep]]=min(f[dep][s+t*power[dep]],f[dep-1][s]);
            f[dep][l]=min(f[dep][l],f[dep-1][s]+val[u]);
        }
    }

    for(int p=adj[u];~p;p=edge[p].next)
        if(!vis[(v=edge[p].v)])
        {
            d[v]=dep+1;
            dfs(v);
            for(int s=0;s<power[dep+1];++s)
                f[dep][s]=min(f[dep+1][s],f[dep+1][s+2*power[dep+1]]);
        }
}

int main()
{
    power[0]=1;
    for(int i=1;i<15;++i)power[i]=power[i-1]*3;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
    {
        scanf("%d",&val[i]);
        adj[i]=-1;
    }
    int u, v;
    while(m--)
    {
        scanf("%d%d",&u,&v);
        add(u,v), add(v,u);
    }
    for(int i=1;i<=n;++i)
        if(!vis[i])
        {
            dfs(i);
            ans+=min(f[0][0],f[0][2]);
        }
    printf("%d\n",ans);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值