[SMOJ1775]岛和桥

题目描述

现在我们知道了一些岛屿,预计连接这些岛屿的桥,一个哈密尔顿路径,就是一条沿着桥梁的路径,经过每个岛屿刚好一次。在我们的地图上,每个岛屿还都有一个相关联的正整数值。如果一条哈密尔顿路径能够使得下面描述的值最大,我们称之为最好的三角哈密尔顿路径。

假设有 n 个岛屿。哈密尔顿路径 C1C2Cn 的值被计算为 3 部分之和。假设 Vi 是岛 Ci 的价值。第 1 部分,我们把哈密尔顿路径中每个岛屿的 Vi 值加起来。第 2 部分,对于路径中的每条边 CiCi+1 ,我们加上乘积 Vi×Vi+1 。第 3 部分,当路径中的三个连续的岛屿 Ci Ci+1 Ci+2 在地图中形成一个三角形时,即 Ci Ci+2 之间有桥,我们加上乘积 Vi×Vi+1×Vi+2

但是可能有有不止一个最好的三角形哈密尔顿路径; 你的第二个任务是找到这样的路径的数量。

输入格式 1775.in

输入文件的第一行以 q q20 )开头,即测试数据组数。每个测试用例以两个整数 n m 开始,分别是岛数和桥数。下一行包含 n 个正整数,第 i 个数字是岛 i Vi 值,每个值不超过 100。以下 m 行的格式为 xy ,表示岛 x 和岛 y 之间有一个(双向)桥。岛从 1 到 n 编号。不会有超过 13 个岛屿。

输出格式 1775.out

对于每个测试用例,输出一个带有两个数字的行,用空格分隔。第一个数字是最佳三角哈密尔顿路径的值; 第二个数字应该是不同的最佳三角哈密尔顿路径的数量。如果测试数据不包含哈密尔顿路径,输出必须为 0 0 。
注意:一条路径倒过来也认为是相同的路径。例如路径 1234 4321 被认为是相同的。

输入样例 1775.in

2
3 3
2 2 2
1 2
2 3
3 1
4 6
1 2 3 4
1 2
1 3
1 4
2 3
2 4
3 4

输出样例 1775.out

22 3
69 1


平心而论,这题算是我到目前为止做的状压 DP 题目见过最难的一道,在 poj 上交了 8 次才过。

题目大意是这样的:有 n 个结点和 m 条边,每个结点有一个固定的价值 vi 。要求找到一条权值最大的哈密顿路径(即遍历完整个图的所有节点,每个节点都要访问到且都只能被访问一次),其中一条哈密顿路径的权值被定义为以下三部分之和:

  1. 所有点的权值之和 vi
  2. vp1vp2 ,其中 p1 p2 是哈密顿路径中所取的每条边相连的两个点
  3. vp1vp2vp3 ,对于所有在原图中可以构成三角形且在哈密顿路径中依次被访问的点 p1 p2 p3

由于最优路径可能不是唯一的,还要统计最优路径的数量。

题目中给的这些提示——岛(结点)和桥(边)很容易就让人往图论的一些算法上想,然而我们很快就会发现没有一个图论算法能够很好的解决这题。

不得不承认,一开始我无论如何都难以将此题与状压 DP 联系起来,直到老师给我们点拨了一下。

对于这类由比较多的部分组成的问题,我们不妨分开考虑,把复杂的几个部分化简。首先第一部分,只要我们能找到一条合法的哈密顿路径,那么肯定包含了所有的点;因此这个是完全可以确定的,也就没有再另外考虑的必要。

对于第二部分,我们不妨定义 f[set][p] 表示在点集 set 中以 p 起点所能取得的最大价值(只对于第二个部分!),那么就有

f[set][p1]=max{vp1vp2+f[set{p1}][p2]}whilep1,p2set

我们看到,式子中出现了子问题 f[set{p1}][p2] ,这是因为我们在确定了起点及与其相连的另一结点后,就可以视作在原图的一个去掉起点的子图中进行计算。
例如,对于下面的图:

假设我们从点 2 出发,下一个点就可以选与 2 相连的 1、3 或 4,那么我们就不再考虑点 2 了,也就意味着 f[{1,2,3,4}][2] 的取值从以下 3 个值中取最大值:

  • v2×v1+f[{1,3,4}][1] (选择了边2->1)(注意边是无向的,下同)
  • v2×v3+f[{1,3,4}][3] (选择了边2->3)
  • v2×v4+f[{1,3,4}][4] (选择了边2->4)

我们发现这样的定义和转移似乎的确是可行的!它符合我们 DP 中要找“子问题”的条件。接着看吧,比方说我们在子集 {1, 3, 4} 中取 1 作为起点:
f[{1,3,4}][1] = max {
v1×v3+f[3,4][3] (选择了边1->3)
v1×v4+f[3,4][4] (选择了边1->4)
}

一直到这里,问题都不大。但是要注意了,对于 f[{3,4}][x] ,还能继续往下分解子问题吗?另外, x 的不同取值会有不同结果吗?

显然不能再分解,也不会有不同结果。因为我们现在考虑的问题必须有至少两个结点,因此在由 3->4 这一条边构成的子图中,起点只能取 3 或者 4,而无论是取 3 或 4 作为起点,得到的都是 v3×v4 或者 v4×v3 ,显然是一样的啊。

于是我们就应该想到了,边界条件就是当集合中只剩下两个点 p1 p2 时,

f[{p1,p2}][p1]=f[{p1,p2}][p2]=vp1×vp2

这样我们就完美地解决了第二部分。那么,第三部分呢?同样的,我们再来单独考虑。

既然我们要考虑相邻的三个点在原图中是否三角形,那么像上面一样只记录一个起点、枚举第二个点显然是不够的,我们就必须要知道前面选取的两个点
其实这种思路的题目我们最近刷得也不少,因此应该有这种顺理成章的思路。(感觉跟中国移动一题的状态记录就有相似之处)

那么我们不妨定义 f[set][a][b] 为在点集 set 中以 (a,b) 起始边所能取得的最大值,那么就有

f[set][p1][p2]=max{f[set{p1}][p2][p3]+vp1vp2vp3}whilep1,p2,p3setand the edges(p1,p2),(p2,p3),(p1,p3)exist

其实与上面的道理是类似的,只不过加多了一维。而且要注意只有当三点之间互相连通时才能加上它们的权值之积,否则就是 0。

这样,我们就分开解决了两个任务。理论上来说,分开解决再合起来计算,并不是不可行;但是这也太麻烦了吧?!为什么不能直接做呢?

类似的,我们仍然定义 f[set][a][b] 为在点集 set 中以 (a,b) 起始边所能取得的最大值,且合并对第二、三部分的计算,那么就有

f[set][p1][p2]=vp1vp2+max{f[set{p1}][p2][p3]f[set{p1}][p2][p3]+vp1vp2vp3 for each p3set if the edge (p1,p3)exist

咦,为什么不用计算 vp2×vp3 呢?因为已经包含在子问题中了。
边界条件也跟之前讲到的是类似的。当 set 中只包含两个点 a b 时,有

f[set][a][b]=f[set][b][a]=va×vb

那么我们最终要求得的解就是 f[{1,2,3,,n}][a][b] 的最大值,其中 a , b 要枚举一下。算法总的时间复杂度为 O(2n×n3)

最后还有一个问题,就是最优路径的数量,可以另开一个数组保存方案数,状态与上面一样。在做 DP 的同时,如果当前值可以更新,那么替换方案数;如果当前最优值与另外一种方法取得的值相同,那么方案数累加一下;取最终答案的时候同理。
还有,题目中正着的路径和反过来视作一样的,既然这是一个无向图,那么最终答案除以 2 就好了。

然后是需要注意的各种细节(血与泪的教训):

  • 记得开 long long
  • 空间范围比较紧,数组开得刚够用就行了
  • 点是从 1 开始编号的,位运算一般都从 0 开始,最好读入的时候统一把编号减1
  • 注意只有一个点的情况
  • 注意不合法的情况

总结一下,有的时候,题目不一定会在形式上显得很 “DP”,最关键的是要去分析问题,看看能不能分解为子问题,看看是否有阶段性无后效性,等等。

参考代码:

#include 
#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int maxn = 13;

int v[maxn];
bool path[maxn][maxn];
LL dp[1 << maxn][maxn][maxn];
LL cnt[1 << maxn][maxn][maxn];

int main(void) {
    freopen("1775.in", "r", stdin);
    freopen("1775.out", "w", stdout);

    int q;
    scanf("%d", &q);
    while (q--) {
        memset(path, false, sizeof path);
        memset(dp, -1, sizeof dp);
        memset(cnt, 0, sizeof cnt);

        int n, m;
        scanf("%d%d", &n, &m);
        for (int i = 0; i < n; i++) scanf("%d", &v[i]);

        for (int i = 0; i < m; i++) {
            int x, y;
            scanf("%d%d", &x, &y); --x; --y; //编号统一从 0 开始
            path[x][y] = path[y][x] = true; //注意是无向边
        }

        if (n == 1) { //1 个点的情况单独讨论
            printf("%d 1\n", v[0]);
            continue;
        }

        int upperLim = 1 << n;
        for (int i = 0; i < upperLim; i++)
            for (int a = 0; a < n; a++)
                if (((1 << a) & i) > 0)
                for (int b = 0; b < n; b++)
                    if (a != b && ((1 << b) & i) > 0 && path[a][b]) {
                        if (i == (1 << a) + (1 << b)) { //set 中仅包含两个点,即边界条件
                            dp[i][a][b] = v[a] * v[b] + v[a] + v[b];
                            cnt[i][a][b] = 1;
                        } else
                            for (int c = 0; c < n; c++)
                                if (a != c && b !=c && ((1 << c) & i) > 0 && path[b][c] && dp[i - (1 << a)][b][c] != -1) {
                                    LL t = dp[i - (1 << a)][b][c] + v[a] * v[b] + v[a];
                                    if (path[a][c]) t += v[a] * v[b] * v[c]; //判断条件 3
                                    //注意一下这里计数的技巧,跟最终统计答案时同理
                                    if (dp[i][a][b] == t) cnt[i][a][b] += cnt[i - (1 << a)][b][c];
                                    else if (dp[i][a][b] < t) {
                                        dp[i][a][b] = t;
                                        cnt[i][a][b] = cnt[i - (1 << a)][b][c];
                                    }
                                }
                    }

        LL ans = 0, count = 0;
        for (int a = 0; a < n; a++)
            for (int b = 0; b < n; b++)
                if (a != b && path[a][b]) {
                    if (dp[upperLim - 1][a][b] > ans) {
                        ans = dp[upperLim - 1][a][b];
                        count = cnt[upperLim - 1][a][b];
                    } else if (dp[upperLim - 1][a][b] == ans) count += cnt[upperLim - 1][a][b];
                }

        printf("%lld %lld\n", ans, count >> 1);
    }
    return 0;
}

PS:

  • SMOJ 上这题第 1~5 个数据点是我出的^_^
  • 看了一下,貌似我的程序在 poj 上跑的是集训队里最快的,才用了 719ms :-p

你可能感兴趣的:(解题报告,SMOJ,状压DP,POJ)