《挑战程序设计竞赛》3.4.1 动态规划-状态压缩DP POJ3311 2686 2411 2441 3254 2836 1795 3411(2)

POJ3311 旅行商问题

http://poj.org/problem?id=3311

题意

给一个起点和终点相同的图,一个矩阵表示各个点之间的距离,求经过所有的点,回到原点的最下路径,点可以重复走。

思路

这个题基本等同于书中例题,唯一的区别是点可以重复走(其实这样对于书中的解法来说,更简单了)。
书中的DP解法是:将已经访问过的节点集合(起点0不算)记为S,当前所在的顶点为v,用dp[S][v]表示从v出发访问剩余的顶点,最终回到顶点0的路径的权重总和的最小值。递推关系式为:
dp[(1<<(n+1))-1][0] = 0;
dp[k][i] = min(dp[k][i], d[i][j] + dp[k | 1 << j][j]);
另外我搜了一下网上很多小伙伴在dp之前先用floyd搜两两之间的距离最小值,我认为是没有必要的,果然我在代码中去掉了floyd也可以AC。
但我试了一个别人的代码去掉floyd就WA,看来跟具体的DP方式有关。见博客:http://blog.csdn.net/weiguang_123/article/details/7908421

代码

Source Code

Problem: 3311       User: liangrx06
Memory: 324K        Time: 0MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int N = 10;
const int INF = 0x3f3f3f3f;

int main(void)
{
    int n;
    int d[N+1][N+1];

    while (cin >> n && n) {
        for (int i = 0; i <= n; i ++) {
            for (int j = 0; j <= n; j ++) {
                scanf("%d", &d[i][j]);
            }
        }
/* for (int k = 0; k <= n; k ++) { for (int i = 0; i <= n; i ++) { for (int j = 0; j <= n; j ++) { d[i][j] = min(d[i][j], d[i][k]+d[k][j]); } } } */
        int dp[1<<(N+1)][N+1];
        for (int k = (1<<(n+1))-1; k >= 0; k --)
            fill(dp[k], dp[k]+n+1, INF);
        dp[(1<<(n+1))-1][0] = 0;
        for (int k = (1<<(n+1))-2; k >= 0; k --) {
            for (int i = 0; i <= n; i ++) {
                for (int j = 0; j <= n; j ++) {
                    dp[k][i] = min(dp[k][i], d[i][j] + dp[k | 1 << j][j]);
                }
            }
        }
        printf("%d\n", dp[0][0]);
    }

    return 0;
}

POJ2686 乘马车旅行

http://poj.org/problem?id=2686

题意

有一个人从某个城市要到另一个城市(城市数量<=30)。
然后有n个马车票,相邻的两个城市走的话要消耗掉一个马车票(马车票数量<=8)。
花费的时间呢,是马车票上有个速率值,用边/速率就是花的时间。
问最后这个人花费的最短时间是多少?不能到达就输出Impossible。

思路

状态压缩DP。马车票的持有状态有2^n种,m个城市,所以用dp[i][j]表示在持有状态为i并在城市j时的已花费最短时间。然后DP循环求解即可。
另外,在我的代码中,最内层的两个循环是可以互换的,已经通过测试。

代码

Source Code

Problem: 2686       User: liangrx06
Memory: 304K        Time: 454MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int N = 8;
const int S = (1<<N);
const int M = 30;
const int INF = 0x3f3f3f3f;

int n, m, p, a, b;
int t[N];
int d[M+1][M+1];

double solve()
{
    int s = (1<<n);
    double dp[S][M+1];

    for (int i = 0; i < s; i ++)
        fill(dp[i], dp[i]+m+1, INF);
    dp[s-1][a] = 0;

    double res = INF;
    for (int i = s-1; i >= 0; i --) {
        for (int j = 1; j <= m; j ++) {
            for (int k = 1; k <= m; k ++) {
                if (d[j][k] != INF) {
                    for (int r = 0; r < n; r ++) {
                        if (i >> r & 1) {
                            dp[i & ~(1<<r)][k] = min(dp[i & ~(1<<r)][k],
                                    dp[i][j] + (double)d[j][k]/t[r]);
                        }
                    }
                }
            }
        }
        res = min(res, dp[i][b]);
    }

    return res;
}

int main(void)
{
    while (cin >> n >> m >> p >> a >> b, n || m || p || a || b) {
        for (int i = 0; i < n; i ++)
            scanf("%d", &t[i]);
        for (int i = 1; i <= m; i ++)
            fill(d[i], d[i]+m+1, INF);
        int x, y, z;
        for (int i = 1; i <= p; i ++) {
            scanf("%d%d%d", &x, &y, &z);
            d[x][y] = d[y][x] = min(z, d[x][y]);
        }

        double ans = solve();
        if (ans >= INF-1)
            printf("Impossible\n");
        else
            printf("%.3lf\n", ans);
    }

    return 0;
}

POJ2411 铺砖问题

http://poj.org/problem?id=2411

题意

给出一个n*m的棋盘,及一个小的矩形1*2,问用这个小的矩形将这个大的棋盘覆盖有多少种方法。

思路

书中例题比这个问题多一个限制条件,就是地板上事先已经涂色,但总体思路基本一致。
既然是课本上的例题,这里就不再重新分析。另外可以参考这篇博客,有比较详细的讲解:状态压缩动态规划 POJ 2411 (编程之美-瓷砖覆盖地板)
值得注意的地方:
DP前先处理一下,交换n和m使n较大m较小,这样能减少状态数。
最后,代码写出来之后运行时间0ms,实在惊讶的很呢

代码

Source Code

Problem: 2411       User: liangrx06
Memory: 276K        Time: 0MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long LL;

const int N = 15;
const int S = (1<<N);

int n, m, s;
LL dp[2][S];

LL solve()
{
    if (m > n) swap(m, n);
    s = 1<<m;
    LL *crt = dp[0], *nxt = dp[1];
    crt[0] = 1;
    for (int i = n-1; i >= 0; i --) {
        for (int j = m-1; j >= 0; j --) {
            for (int k = 0; k < s; k ++) {
                if (k & 1<<j) {
                    nxt[k] = crt[k & ~(1<<j)];
                }
                else {
                    LL ans = 0;
                    if (j < m-1 && !(k & 1<<(j+1))) {
                        ans += crt[k | 1<<(j+1)];
                    }
                    if (i < n-1) {
                        ans += crt[k | 1<<j];
                    }
                    nxt[k] = ans;
                }
            }
            swap(crt, nxt);
        }
    }
    return crt[0];
}

int main(void)
{
    while (cin >> n >> m, n || m) {
        printf("%lld\n", solve());
    }

    return 0;
}

POJ2441 安排牛棚

http://poj.org/problem?id=2441

题意

n头牛,m个位置,每头牛有各自喜欢的位置,问安排这n头牛使得每头牛都在各自喜欢的位置有几种安排方法。

思路

明显是状态DP,但一开始没有想好内外层循环的顺序,思路有点乱。后来想明白了:

用dp[i][j]表示安排好前i头牛,牛栏的未使用状态为j时的安排方法数。

这样以i作为外层循环,j作为内层循环DP即可(具体见代码)。
当然这样肯定会超内存,用滚动数组,dp数组大小只要开成[2][2^m]即可。
然后提交之后TLE了,分析后发现主要原因是内层循环中,其实只需要搜索还剩m-i个牛栏未使用(也就是已经使用了i个)的情况。这样只要枚举集合中元素个数为m-i的子集即可,不需要全部枚举。

位运算枚举集合中元素个数为k的子集,见《挑战》第二版书157页,网上有一篇帖子也有介绍:集合元素的排列与子集
我在参照该代码写时不慎将-和~号混淆了,导致很长时间查不出错误,谨记!谨记!谨记!

优化后提交发现RE了,找了下原因是n>m时会内存访问错误,所以n>m的情况应该单独处理,这种情况下ans=0。
然后提交就能够AC了,只要280ms。
最后,优化后就没必要用滚动数组了,因为更新的值不会重叠。修改为一维数组后继续优化至230ms,内存也降低了一半。

另外还可以参考这篇博客,与我的DP思路基本一致,但他的代码多一些注解:poj 2441 Arrange the Bulls(状态压缩DP)

代码

Source Code

Problem: 2441       User: liangrx06
Memory: 4348K       Time: 235MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int N = 20;
const int M = 20;
const int S = 1<<M;

struct Cow {
    int p;
    int b[M];
};

int n, m;
Cow c[N];
int dp[S];

int main(void)
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++) {
        scanf("%d", &c[i].p);
        for (int j = 0; j < c[i].p; j ++) {
            scanf("%d", &c[i].b[j]);
            c[i].b[j] --;
        }
    }

    int ans = 0;
    if (n <= m) {
        int s = 1<<m;
        fill(dp, dp+s, 0);
        dp[s-1] = 1;
        for (int i = 0; i < n; i ++) {
            ans = 0;
            for (int k = (1<<(m-i))-1; k < s; ) {
                for (int j = 0; j < c[i].p; j ++) {
                    int r = c[i].b[j];
                    if (k & 1<<r) {
                        dp[k & ~(1<<r)] += dp[k];
                        ans += dp[k];
                    }
                }
                int x = k & -k, y = k + x;
                k = ((k & ~y) / x >> 1) | y;
            }
        }
    }
    printf("%d\n", ans);

    return 0;
}

POJ3254 种植土地

http://poj.org/problem?id=3254

题意

给出一个n行m列的地,1表示肥沃,0表示贫瘠,现在在肥沃的地上种植物,相邻的两块地不能同时种,问你有多少种放法。

思路

与POJ2411铺砖问题比较类似,但这个题的限制条件是相邻的两块地不能同时种,也就是受之前的种地情况影响,因而循环顺序应该与2411题相反,从小坐标向大坐标做DP。
最后的结果是所有状态相加。
需要注意的几个地方:
(1)如果行数小于列数,应该将行列置换,可减少时空复杂度。因为状态数为2^列数,是主要影响因子。果然我加了这个优化后时间从32ms降到16ms。
(2)滚动数组交换后要将nxt数组清零。否则会出现错误。
另外可参考分析的比较详细的一篇博客:Poj - 3254 Corn Fields详解

代码

Source Code

Problem: 3254       User: liangrx06
Memory: 252K        Time: 16MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int N = 12;
const int S = (1<<N);
const int MOD = 100000000;

int n, m, s;
bool grass[N][N];
int dp[2][S];

int solve()
{
    s = 1<<m;
    int *crt = dp[0], *nxt = dp[1];
    fill(crt, crt+s, 0);
    crt[0] = 1;
    for (int i = 0; i < n; i ++) {
        for (int j = 0; j < m; j ++) {
            fill(nxt, nxt+s, 0);
            for (int k = 0; k < s; k ++) {
                int nk = k & ~(1<<j);
                nxt[nk] = (nxt[nk] + crt[k]) % MOD;
                if ((grass[i][j]) && !(j > 0 && (k & 1<<(j-1))) && !(i > 0 && (k & 1<<j))) {
                    nk = k | 1<<j;
                    nxt[nk] = (nxt[nk] + crt[k]) % MOD;
                }
            }
            swap(crt, nxt);
        }
    }
    int ans = 0;
    for (int k = 0; k < s; k ++)
        ans = (ans + crt[k]) % MOD;
    return ans;
}

int main(void)
{
    while (cin >> n >> m) {
        int tmp;
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < m; j ++) {
                scanf("%d", &tmp);
                if (n >= m) grass[i][j] = tmp ? true : false;
                else grass[j][i] = tmp ? true : false;
            }
        }
        if (n < m) swap(n, m);
        printf("%d\n", solve());
    }

    return 0;
}

POJ2836

http://poj.org/problem?id=2836

题意

思路

代码

POJ1795

http://poj.org/problem?id=1795

题意

思路

代码

POJ3411 道路费用问题

http://poj.org/problem?id=3411

题意

给定一个N和M,N代表城市数目(城市以1-N命名),其中有M条边连接这些城市,城市之间可能有重边。接下来有M行。每行有5个输入,分别为ai,bi,ci,pi和ri。ai表示第i条边的起始城市,bi表示第i条边的末尾城市。经过每条边都需要付钱,有两种付钱方式,付钱数分别为pi和ri,当且仅当ci这个城市之前有经过,才可以用ri这种付钱方式。然后要求找出一条付钱数最少的从城市1到城市N的路径。

思路

这道题与POJ3311、2686均有相似之处,一开始我就直接在2686的代码上进行修改。
城市的访问状态有2^n种,所以用dp[i][j]表示在已经访问的城市集合为i,并现在位于在城市j时的最小花费。然后DP循环求解即可。由于城市之间的重边无法比较大小(有p和r两个可能花费),因此需要将重边保存到vector中,边结构体只需要存储c,p,r就可以了。
在搜索某一条边时,if (i>>(roads[j][k][l].c) & 1)表示此时已经访问过c,可以按p缴费。而题目说明p<=r,所以没有必要再对r检验。而如果else则表示只能按r缴费,这时检验r即可。
另外,搜索了一下网上其他人的代码,没有发现有用循环来做的,都是DFS+记忆化搜索(尽管跟状态压缩DP思想一致),所以我写的状态压缩DP应该说最标准。

这个题最大的难点在于:路径可能需要重复走才能达到费用最优
绝大多数的coder都是错在这个地方。其实题目数据就能给出提示,另外更详细的说明解释见博客:POJ3411-Paid Roads。
在博客中作者给出了比例子更强的一组测试数据:
6 5
1 2 1 10 10
2 3 4 10 100
2 4 2 15 15
4 1 1 12 12
3 6 6 10 10
并在博文的最后指出:

同一条路可以重复走,但是不能无限重复走,重复的次数是有限的。那么应该重复多少次才合理?这与m值有关。题目的m值范围为<=10,那么当人一个城市被到达的次数若 >3次(不包括3),所走的方案必然出现了环路(网上的同学称之为“闸数”)

在另外的文章中,对最少重复次数的讨论观点多是2次或3次,但均没有人给出确定的理论解释。我的代码中重复2次(cnt变量控制循环)这个题就能够AC。
那么重复2次是不是就满足所有的情况呢?还是这个题的数据仍然不够强?期待牛人给出更准确的答案。

代码

Source Code

Problem: 3411       User: liangrx06
Memory: 288K        Time: 0MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 10;
const int S = (1<<N);
const int INF = 0x3f3f3f3f;

struct Road {
    int c, p, r;
};
typedef vector<Road> Roads;

int n, m, s;
Roads roads[N][N];
int dp[S][N];

void solve()
{
    for (int i = 0; i < s; i ++)
        fill(dp[i], dp[i]+n, INF);
    dp[1][0] = 0;

    int res = INF;
    for (int i = 1; i < s; i ++) {
        for (int cnt = 0; cnt < 2; cnt ++) {
            for (int j = 0; j < n; j ++) {
                for (int k = 0; k < n; k ++) {
                    if (roads[j][k].size()) { // from j to k
                        for (int l = 0; l < roads[j][k].size(); l ++) {
                            if (i>>(roads[j][k][l].c) & 1) {
                                dp[i | 1<<k][k] = min(dp[i | 1<<k][k],
                                        dp[i][j] + roads[j][k][l].p);
                            }
                            else { //because p <= r
                                dp[i | 1<<k][k] = min(dp[i | 1<<k][k],
                                        dp[i][j] + roads[j][k][l].r);
                            }
                        }
                    }
                }
            }
        }
        res = min(res, dp[i][n-1]);
    }

    if (res == INF)
        printf("impossible\n");
    else
        printf("%d\n", res);
}

int main(void)
{
    while (cin >> n >> m) {
        s = 1<<n;
        int a, b;
        Road road;
        for (int i = 0; i < m; i ++) {
            scanf("%d%d%d%d%d", &a, &b, &road.c, &road.p, &road.r);
            road.c --;
            roads[a-1][b-1].push_back(road);
        }

        solve();
    }

    return 0;
}

你可能感兴趣的:(动态规划,状态压缩dp,挑战程序设计竞赛)