0/1背包问题是个典型问题,其解法有很多,如回溯法、分枝限界法、动态规划法、递归策略等 0/1背包问题 0/1背包问题是背包问题中最基本的一种,其状态转移方程:m[i][j] = Max(m[i+1][j] , m[i+1][j - w[i]] + v[i]) 对比了自己的代码和王晓东书上的代码,感觉在两方面值得自己学习: 1. jMax = Min(w[i] - 1 , c); 这样写法使得在(0 ~ jMax)可以直接赋值,无需判断,因此减少了判断次数; 2. 对于求m[1][c],并不是放在循环体里,而是单独拎出来求,这样就避免了求解很多“无意义”情况; #include <stdio.h> #define Min(a,b) ((a) < (b) ? (a) : (b)) #define Max(a,b) ((a) > (b) ? (a) : (b)) #define N 20 int m[N][N] , v[N] , w[N] , n , c; int Knapsack() { int i , j , jMax; jMax = Min(w[n] - 1 , c); for(j = 0 ; j <= jMax ; j++) m[n][j] = 0; for(j = w[n] ; j <= c ; j++) m[n][j] = v[n]; for(i = n - 1 ; i > 1 ; i--) { jMax = Min(w[i] - 1 , c); for(j = 0 ; j <= jMax ; j++) m[i][j] = m[i+1][j]; for(j = w[i] ; j <= c ; j++) m[i][j] = Max(m[i+1][j] , m[i+1][j - w[i]] + v[i]); } m[1][c] = m[2][c]; m[1][c] = Max(m[2][c] , m[2][c - w[1]] + v[1]); return m[1][c]; } void TraceBack(int *x) { int i , j; for(i = 1 , j = c ; i < n ; i++) { if(m[i][j] == m[i+1][j]) { x[i] = 0; } else { x[i] = 1; j -= w[i]; } } x[n] = m[n][j] ? 1 : 0; } int main(void) { int x[N] , i; scanf("%d%d", &c , &n); //背包容量,背包个数 for(i = 1 ; i <= n ; i++) scanf("%d", w + i); //物品重量 for(i = 1 ; i <= n ; i++) scanf("%d", v + i); //物品价值 printf("Max Value %d\n", Knapsack()); TraceBack(x); printf("选择的物品:"); for(i = 1 ; i <= n ; i++) { if(x[i]) printf("重量(%d)_价值(%d) ", w[i] ,v[i]); } return 0; } 上面程序的时间复杂度和空间复杂度都是O(nc)。时间复杂度我们已经无法再优化了,但是空间复杂度我们还是可以降低到O(c)。 理由就是:每次求m[i][j]只需要利用前一行数据m[i+1][0~c],因此只要维护一个一维数组m[0~c]即可(也可以维护一个二维数组m[2][c],然后循环滚动赋值) int Knapsack() { int i , j , jMax; jMax = Min(w[n] - 1 , c); for(j = 0 ; j <= jMax ; j++) m[j] = 0; for(j = w[n] ; j <= c ; j++) m[j] = v[n]; for(i = n - 1 ; i > 1 ; i--) { for(j = c ; j >= w[i] ; j--) { m[j] = Max(m[j] , m[j - w[i]] + v[i]); } } m[c] = Max(m[c] , m[c - w[1]] + v[1]); return m[c]; } 1.01-package (最直接应用) 题目描述:给定一个背包的容量k,给定n个物品的体积和价值,物品不可分割,将n个物品中选若干个物品放入背包,求背包内物品的最大价值总和,在价值总和最大的前提下求背包内的最小物品个数c。 分析:求最大价值的方法我们在前面已经分析过了,现在只要知道如何求最小物品个数。如果放入当前物品i 使总价值增加,那么当前物品数为:cnt[j] = cnt[j-w[i]] + 1(j为背包容量) ;如果放入当前物品不会对总价值造成影响,那么我们就要找“最小物品”即,cnt[j] = Min(cnt[j] , cnt[j - w[i]] + 1) ; 代码 #include <stdio.h> #include <string.h> #define Min(a,b) ((a) < (b) ? (a) : (b)) #define N 2000 int m[N] , cnt[N] , w[N] , v[N] , n , c; void Knapsack() { int i , j , jMax; jMax = Min(w[n] - 1 , c); for(j = 0 ; j <= jMax ; j++) {m[j] = 0; cnt[j] = 0;} for(j = w[n] ; j <= c ; j++) {m[j] = v[n]; cnt[j] = 1;} for(i = n - 1 ; i > 1 ; i--) { for(j = c ; j >= w[i] ; j--) { if(m[j] < m[j - w[i]] + v[i]) { m[j] = m[j - w[i]] + v[i]; cnt[j] = cnt[j-w[i]] + 1; } else if(m[j] == m[j - w[i]] + v[i]) { cnt[j] = Min(cnt[j] , cnt[j - w[i]] + 1); } } } if(m[c] < m[c - w[1]] + v[1]) { m[c] = m[c - w[1]] + v[1]; cnt[c] = cnt[c-w[1]] + 1; } else if(m[c] == m[c - w[1]] + v[1]) cnt[c] = Min(cnt[c] , cnt[c - w[1]] + 1); } int main(void) { int z , i; scanf("%d", &z); while(z-- > 0) { scanf("%d%d", &n,&c); for(i = 1 ; i <= n ; i++) scanf("%d%d", v + i , w + i); Knapsack(); printf("%d %d\n", m[c] ,cnt[c]); } return 0; 2.Incredible Cows 这道题实际可以抽象成:把一组数分成两个集合,使得这两个集合和的绝对值差最小。 分析:分成两个集合,那么一个数要么放在集合1,要么放在集合2,也就是, x[i] = 0 : 第i个数放入第1个集合 x[i] = 1 :第i个数放入第2个集合 这显然是个0/1背包问题,这里的物体重量w[i]就是i本身,且w[i]==v[i] ,(这题由于C较大用回溯法解决01背包而不是DP) 代码 #include <stdio.h> #define Max(a,b) ((a) > (b) ? (a) : (b)) #define N 35 int m[N] , w[N] , n , c , cw , best; int Bound(int i) { int cleft = c - cw; int b = cw; while(i <= n && w[i] <= cleft) { cleft -= w[i]; b += w[i]; i++; } if(i <= n) b += cleft; return b; } void Knapsack(int i) { if(i > n) { best = Max(cw , best); return; } if(cw + w[i] <= c) //left { cw += w[i]; Knapsack(i + 1); cw -= w[i]; } if(Bound(i+1) > best) //right(剪枝) { Knapsack(i + 1); } } void QuickSort(int *arr , int left , int right) { int i , j , x , nTemp; if(left >= right) //边界条件检查 return; else { //Partition i = left; j = right + 1; x = arr[i]; while(1) { do i++; while(i < j && arr[i] > x); do j--; while(arr[j] < x); if(i > j) break; //swap(i,j) nTemp = arr[i]; arr[i] = arr[j]; arr[j] = nTemp; } //swap(left,j) nTemp = arr[left]; arr[left] = arr[j]; arr[j] = nTemp; QuickSort(arr,left,j-1); QuickSort(arr,j+1,right); } } int main(void) { int z , i , k , j; scanf("%d", &z); while(z-- > 0) { k = cw = best = 0; scanf("%d", &n); for(i = 1 ; i <= n ; i++) { scanf("%d" , w + i); k += w[i]; } QuickSort(w,1,n); c = k / 2 + (k & 1); //背包容量 Knapsack(1); j = k - best; printf("%d\n", j > best ? j - best : best - j); } return 0; } 总结:以后凡是遇到这种子集选取的问题,都可以抽象成01背包问题来解决(其中v[i]往往等于w[i])。