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;
}
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;
}
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;
}
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;
}
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;
}
http://poj.org/problem?id=2836
http://poj.org/problem?id=1795
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;
}