[2019 CSP-S赛前集训] [P3959] [蒟蒻Xx_queue学DP] 3.宝藏

蒟蒻Xx_queue又来更博客啦!!!

话说之前一直都没有讲到与状压DP枚举子集有关的知识,那我们今天就来补充一下;

今天要讲的题目是:

[NOIP2017TG] 宝藏


题目传送门

题目描述

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了\(n\)个深埋在地下的宝藏屋, 也给出了这\(n\)个宝藏屋之间可供开发的\(m\)条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发

新开发一条道路的代价是:

\(\mathrm{L} \times \mathrm{K}\)

\(L\)代表这条道路的长度,\(K\)代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值

输入格式

第一行两个用空格分离的正整数 \(n\),\(m\),代表宝藏屋的个数和道路数。

接下来 \(m\) 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏 屋的编号(编号为 \(1 - n\)),和这条道路的长度 \(v\)

输出格式

一个正整数,表示最小的总代价。

输入输出样例

输入 1
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1

输出 1
4

输入 2
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 2
输出 2
5

说明/提示

【样例解释1】

小明选定让赞助商打通了\(1\) 号宝藏屋。小明开发了道路 \(1 \to 2\),挖掘了 \(2\) 号宝 藏。开发了道路 \(1 \to 4\),挖掘了 \(4\) 号宝藏。还开发了道路 \(4 \to 3\),挖掘了\(3\)号宝 藏。工程总代价为:\(1 \times 1 + 1 \times 1 + 1 \times 2 = 4\)

【样例解释2】

小明选定让赞助商打通了\(1\) 号宝藏屋。小明开发了道路 \(1 \to 2\),挖掘了 \(2\) 号宝 藏。开发了道路 \(1 \to 3\),挖掘了 \(3\) 号宝藏。还开发了道路 \(1 \to 4\),挖掘了\(4\)号宝 藏。工程总代价为:\(1 \times 1 + 3 \times 1 + 1 \times 1 = 5\)

【数据规模与约定】

对于20%的数据: 保证输入是一棵树\(1≤n≤8\)\(v≤5000\) 且所有的 \(v\)都相等。

对于 40%的数据: \(1≤n≤8\)\(0≤m≤1000\)\(v≤5000\) 且所有的\(v\)都相等。

对于70%的数据: \(1≤n≤8\)\(0≤m≤1000\)\(v≤5000\)

对于100%的数据: \(1≤n≤12\)\(0≤m≤1000\)\(v≤500000\)


分析:

先看数据范围:\(1≤n≤12\),不是爆搜就是状压DP;(爆搜加玄学剪枝当然可以过,详解见洛谷题解)

(玄学的模拟退火就算了,不知道你们有没有想到最小生成树,然而是行不通的)

这里我就来讲一讲状压DP的做法,毕竟前面我才刚刚写了一个状压DP的学习总结嘛(这回不是玉米田了);

因为题目中说到:新开发一条道路的代价是:

\(\mathrm{L} \times \mathrm{K}\)

\(L\)代表这条道路的长度,\(K\)代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋。

所以这个题目应该和你打通的数的层数有关系(因为你需要计算K啊);

所以不妨设\(f_{i,j}\)表示已经打通的宝藏屋中的最大深度为\(i\),宝藏屋打通的状态(状压,二进制,大概明白是什么意思吧?)为\(j\),

\(j\)的每一位中1代表这个宝藏屋已经打通了,0表示没有打通;

现有的状态可以由 这个状态的子集+新打通的一些宝藏屋 转移过来

所以比较容易得出状态转移方程就是\(f_{i,j}=min\{f_{i-1,k}+(i-1)*c_{k,j}\}\)

其中\(i\)为深度,\(j\)为目标状态,\(k\)\(j\)的子集,\(c_{k,j}\)代表从状态\(k\)转移至\(j\)的花费(也就是把集合\(j\)的子集\(k\)添加一部分元素变成集合\(j\)所需要的代价)

蒯了一个图,大家看一看:(来自洛谷题解)

大概就是这样吧,不知道你搞懂了吗??

这里注意一些细节:我们需要预处理出从状态\(k\)转移至\(j\)的花费(下面的代码中用trans数组储存),还要记得开long long!

具体细节以及实现过程请研究代码:


AC代码:

#include <bits/stdc++.h>
#define N 13
#define INF 0x3f3f3f3f
using namespace std;
int n,m;
int dis[N][N];//两个宝藏屋之间的距离
int trans[1<<N][1<<N];//从一个状态转移至另一个状态(从集合的子集转移至该集合)的花费
long long dp[N][1<<N];//dp[i][j],i表示深度,j表示状态(二进制数)
void gettrans(){//取得转移花费
    int siz=(1<<n);
    for(int i=0;i<siz;i++){
        for(int j=i;j!=0;j=(j-1)&i){//枚举i的子集
            bool rightt=true;
            int now=i^j;//now为i-j(集合相减),得到的是j中不含而i中含有的元素
            for(int k=n;k>=1;k--){//枚举集合now的每一位
                if(now>=(1<<(k-1))){//now的第k位是否为1
                    int minn=INF;
                    for(int l=1;l<=n;l++)
                        if(((1<<(l-1))&j)==(1<<(l-1))) minn=min(minn,dis[l][k]);//求出转移花费
                    if(minn==INF){
                        rightt=false;
                        break;
                    }
                    trans[j][i]+=minn;
                    now-=(1<<(k-1));//继续求
                }
            }
            if(rightt==false) trans[j][i]=INF;//转移不了视为inf
        }
    }
}
void DP(){//dp
    int siz=(1<<n);
    memset(dp,0x3f,sizeof(dp));//初始化为inf
    for(int i=1;i<=n;i++){
        dp[1][1<<(i-1)]=0;//只有一层,则为0(免费开采第一层)
    }
    for(int i=2;i<=n;i++){
        for(int j=0;j<siz;j++){
            for(int k=j;k!=0;k=(k-1)&j){//枚举子集
                if(trans[k][j]!=INF) dp[i][j]=min(dp[i][j],dp[i-1][k]+trans[k][j]*(i-1));//转移方程(前面详细讲过了,不知道的回炉重造)
            }
        }
    }
}
int main(){
    scanf("%d%d",&n,&m);
    memset(dis,0x3f,sizeof(dis));
    for(int i=1;i<=m;i++){//读入不用解释
        int u,v,wei;
        scanf("%d%d%d",&u,&v,&wei);
        if(wei<dis[u][v]){
            dis[u][v]=dis[v][u]=wei;
        }
    }
    gettrans();
    DP();
    long long ans=0x3f3f3f3f3f3f3f3f;
    int siz=(1<<n);
    for(int i=1;i<=n;i++) ans=min(ans,dp[i][siz-1]);//统计答案
    printf("%lld",ans);
    return 0;
}

怎么样?除了玉米田,可见枚举子集也是非常非常重要的(当然还有集合的基本运算),相信你弄懂这道题目后对于状压DP有更加理解了;

还是要多多练习啊!


猜你喜欢

转载自www.cnblogs.com/Xx-queue/p/11755817.html
今日推荐