现在我们知道了一些岛屿,预计连接这些岛屿的桥,一个哈密尔顿路径,就是一条沿着桥梁的路径,经过每个岛屿刚好一次。在我们的地图上,每个岛屿还都有一个相关联的正整数值。如果一条哈密尔顿路径能够使得下面描述的值最大,我们称之为最好的三角哈密尔顿路径。
假设有 n 个岛屿。哈密尔顿路径 C1→C2→…→Cn 的值被计算为 3 部分之和。假设 Vi 是岛 Ci 的价值。第 1 部分,我们把哈密尔顿路径中每个岛屿的 Vi 值加起来。第 2 部分,对于路径中的每条边 Ci→Ci+1 ,我们加上乘积 Vi×Vi+1 。第 3 部分,当路径中的三个连续的岛屿 Ci , Ci+1 , Ci+2 在地图中形成一个三角形时,即 Ci 和 Ci+2 之间有桥,我们加上乘积 Vi×Vi+1×Vi+2 。
但是可能有有不止一个最好的三角形哈密尔顿路径; 你的第二个任务是找到这样的路径的数量。
输入文件的第一行以 q ( q≤20 )开头,即测试数据组数。每个测试用例以两个整数 n 和 m 开始,分别是岛数和桥数。下一行包含 n 个正整数,第 i 个数字是岛 i 的 Vi 值,每个值不超过 100。以下 m 行的格式为 xy ,表示岛 x 和岛 y 之间有一个(双向)桥。岛从 1 到 n 编号。不会有超过 13 个岛屿。
对于每个测试用例,输出一个带有两个数字的行,用空格分隔。第一个数字是最佳三角哈密尔顿路径的值; 第二个数字应该是不同的最佳三角哈密尔顿路径的数量。如果测试数据不包含哈密尔顿路径,输出必须为 0 0 。
注意:一条路径倒过来也认为是相同的路径。例如路径 1→2→3→4 和 4→3→2→1 被认为是相同的。
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
22 3
69 1
平心而论,这题算是我到目前为止做的状压 DP 题目见过最难的一道,在 poj 上交了 8 次才过。
题目大意是这样的:有 n 个结点和 m 条边,每个结点有一个固定的价值 vi 。要求找到一条权值最大的哈密顿路径(即遍历完整个图的所有节点,每个节点都要访问到且都只能被访问一次),其中一条哈密顿路径的权值被定义为以下三部分之和:
由于最优路径可能不是唯一的,还要统计最优路径的数量。
题目中给的这些提示——岛(结点)和桥(边)很容易就让人往图论的一些算法上想,然而我们很快就会发现没有一个图论算法能够很好的解决这题。
不得不承认,一开始我无论如何都难以将此题与状压 DP 联系起来,直到老师给我们点拨了一下。
对于这类由比较多的部分组成的问题,我们不妨分开考虑,把复杂的几个部分化简。首先第一部分,只要我们能找到一条合法的哈密顿路径,那么肯定包含了所有的点;因此这个是完全可以确定的,也就没有再另外考虑的必要。
对于第二部分,我们不妨定义 f[set][p] 表示在点集 set 中以 p 为起点所能取得的最大价值(只对于第二个部分!),那么就有
我们看到,式子中出现了子问题 f[set−{p1}][p2] ,这是因为我们在确定了起点及与其相连的另一结点后,就可以视作在原图的一个去掉起点的子图中进行计算。
例如,对于下面的图:
假设我们从点 2 出发,下一个点就可以选与 2 相连的 1、3 或 4,那么我们就不再考虑点 2 了,也就意味着 f[{1,2,3,4}][2] 的取值从以下 3 个值中取最大值:
我们发现这样的定义和转移似乎的确是可行的!它符合我们 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[set][a][b] 为在点集 set 中以 (a,b) 为起始边所能取得的最大值,那么就有
其实与上面的道理是类似的,只不过加多了一维。而且要注意只有当三点之间互相连通时才能加上它们的权值之积,否则就是 0。
这样,我们就分开解决了两个任务。理论上来说,分开解决再合起来计算,并不是不可行;但是这也太麻烦了吧?!为什么不能直接做呢?
类似的,我们仍然定义 f[set][a][b] 为在点集 set 中以 (a,b) 为起始边所能取得的最大值,且合并对第二、三部分的计算,那么就有
咦,为什么不用计算 vp2×vp3 呢?因为已经包含在子问题中了。
边界条件也跟之前讲到的是类似的。当 set 中只包含两个点 a 和 b 时,有
那么我们最终要求得的解就是 f[{1,2,3,…,n}][a][b] 的最大值,其中 a , b 要枚举一下。算法总的时间复杂度为 O(2n×n3) 。
最后还有一个问题,就是最优路径的数量,可以另开一个数组保存方案数,状态与上面一样。在做 DP 的同时,如果当前值可以更新,那么替换方案数;如果当前最优值与另外一种方法取得的值相同,那么方案数累加一下;取最终答案的时候同理。
还有,题目中正着的路径和反过来视作一样的,既然这是一个无向图,那么最终答案除以 2 就好了。
然后是需要注意的各种细节(血与泪的教训):
总结一下,有的时候,题目不一定会在形式上显得很 “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: