14 P3959 NOIP2017提高组 宝藏 题解

NOIP2017提高组 宝藏

题面

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

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

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

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

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

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

输入 #1

4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1

输出 #1

4

对于 $ 100%$ 的数据: \(1 \le n \le 12\)\(0 \le m \le 10^3\)\(v \le 5\times 10^5\)

题解

这道题对于要打通的一个点来说,我们要区分这个点在哪一层,所以我们考虑每次枚举这一整层的点有哪些,可以状压

\(f(i,j)\) 表示当前已经分好层的节点集合为 \(i\) ,要将节点集合为 \(j\) 的节点分到下一层的最小代价(这里计算的时候这个点可能不一定在这一层,但如果可以在更靠前的层,那么会在其他状态枚举到,所以不用担心出错)

\(f\) 我们可以 \(O(3^nn)\) 的计算出来,首先我们 \(O(3^n)\) 枚举出 \(i\)\(j\) ,然后倒序枚举 \(j\) ,也就是从小到大枚举 \(j\) ,然后计算 \(\log lowbit(j)\) 到集合 \(i\) 中点的最小距离,这样我们每次枚举一个 \(j\) 就只会算 \(n\) 次,那么时间复杂度就是 \(O(n)\) ,注意,这里的倒序是关键

\(g(l,i)\) 表示已经选了 \(l\) 层,选点的集合为 \(i\) 的最小代价,根据 \(f\) 的定义,我们不难得出转移方程

\(g(l,i) = min \{g(l - 1, i \oplus j) + f(i \oplus j, j) * l\}\)

枚举 \(l\)\(O(n)\) ,枚举 \(i\)\(j\)\(O(3^n)\) ,总时间复杂度 \(O(3^n n)\)

code

枚举子集的代码,多看看就会了

for (int i = s; i; i = (i - 1) & s)

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>

using namespace std;

typedef long long ll;

const int N = 13, M = (1 << 12) + 7;

int n, m;
int a[N][N], f[M][M], g[N][M];
int ne[M];

/*
f[i][j] : 表示已选集合 i ,将集合 j 加进来的最小代价
g[l][i] : 表示填到第 l 层,集合为 i 的最小代价
*/

int main () {
    cin >> n >> m;
    //注意这里对 a 的初始化,要初始化成一个不太大的值
    memset (a, 0b0001, sizeof a);
    memset (g, 0x3f, sizeof g);
    memset (f, 0x3f, sizeof f);

    for (int i = 1; i <= m; i ++) {
        int x, y, v;
        cin >> x >> y >> v;
        x --, y --;
        a[x][y] = min (a[x][y], v);
        a[y][x] = min (a[y][x], v);
    }

    int mn = (1 << n) - 1;
    for (int i = 1; i <= mn; i ++) {
        f[i][0] = 0;
    }

    //预处理 f 数组
    for (int i = 1; i <= mn; i ++) {
        int t = 0;
        int s = mn ^ i;
        for (int j = s; j; j = (j - 1) & s) {
            ne[j] = t;
            t = j;
        }
        for (int j = t; j; j = ne[j]) {
            int pos = __lg (j & -j);
            int dis = 2e9;
            for (int k = 0; k < n; k ++) {
                if ((i >> k) & 1) {
                    dis = min (dis, a[pos][k]);
                }
            }
            //后面这里加的时候最多加 12 次,如果每次都是最大的 a ,要考虑是否会溢出
            f[i][j] = f[i][j ^ (j & -j)] + dis;
        }
    }
    
    for (int i = 1; i <= mn; i <<= 1) {
        g[0][i] = 0;
    }
    
    for (int l = 1; l < n; l ++) {
        for (int i = 1; i <= mn; i ++) {
            int s = i;
            for (int j = s; j; j = (j - 1) & s) {
                g[l][i] = min ((ll)g[l][i], (ll)g[l - 1][i ^ j] + (ll)f[i ^ j][j] * l);
            }
        }
    }

    int ans = 2e9;
    for (int l = 0; l <= n; l ++) {
        ans = min (ans, g[l][mn]);
    }
    cout << ans << endl;

    return 0;
}
posted @ 2025-10-05 18:01  michaele  阅读(16)  评论(0)    收藏  举报