动态规划(背包问题)

         

     背包问题


        背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。

        Example:

        在下面的图中,应该选择哪些盒子,才能使得价格尽可能的大,而保持重量小于或等于15KG?

        图片来源:维基百科. 


     0/1背包问题:

 
        背包问题有很多变形,0/1背包,无界背包,等。我们本文就记录下0/1背包中的各种问题与解决问题的思想和代码。
        所谓0/1背包问题,就是每种物品只会放进背包0个或者1个,一个物品要么整个不放入背包,要么整个放入背包,物品无法切割。规范化的0/1背包问题:

        图片来源:维基百科

 

        像这种求最优解的题目一般都可以用动态规划来解决的,如果我们要用动态规划来做,我们需要找到的状态和状态转移方程。  我们以上图中的问题为例,现在有5个元素可供我们选择,对当前的任何一个元素我们有两种方式,放入背包(不放入背包),0/1背包的关键点就是如何有效的利用背包的剩余重量,找出最好的物品组合方式。使得其中物品价值最大。


        下面可以一步一步分析,当前有5个元素,我们对其中的每个元素都做两种选择,放入或者不放入背包。

         1首先我们把价值为10的那个放入背包,书包价值+10 = 10,重量-4 = 11,余下的子问题是如何在剩下的4个元素中组合元素使得重量为11的包价值最大。

         2.我们可以不把价值为10的那个元素放入背包,书包价值=0,重量=15,余下的子问题就是如何在剩下的4个元素中组个元素使得重量为15的包价值最大。

        写到这里应该可以看出来,这符合动态规划的特征:最优子结构,下一步我们需要形式化的来表达出来原问题与子问题的关系

        定义 dp( n,  w )代表从n个元素中选择组合方式使得重量为w的背包价值最大,value( n )表示第n个元素的价值,weight( n )表示第n个元素的重量。则可以得到状态转移方程:

    dp(n,w) = max( dp(n-1,w), dp(n-1,w-weight(n))+value(n))
//  dp(n-1,w)表示物品不放入背包,从剩下n-1个元素中,书包重量为w中获取最大价值
//  dp(n-1,w-weight(n))+value(n)表示第n个元素放入书包中(w > w - weight(n)

        然后我们去构造dp数组,构造完成之后dp( n, w ) 即为我么想要的答案。0/1背包可以有很多问题,例如求背包可获取的最大价值,背包中有哪些物品,背包的物品有哪些不同的组合方式.下面用代码的方式一一解决这些问题。

        

     计算背包可获取的最大的价值.


        状态转移方程我们已经知道了,下面我们需要做得就是将状态的表填充满dp[n][w],下一步直接输出dp[n][w]就可以了。

        状态表:
        图片来源:百度图片.


        Code:bottom-up (自底向上计算解)

const int N = 100; //物品个数
const int M = 10000; //背包重量

int value[ N ],weight[ N ]; //物品价值和重量
int dp[ N + 1 ][ M + 1 ]; //dp数组

//构造dp数组
void knapsack( int n, int w ){//n表示个数,w表示重量
    
    memset( dp, 0, sizeof( dp ) );
    int i, j;
    for ( i = 1; i <= n; i++ ) { //穷举每个物品,物品编号从1开始
        for ( j = 0; j <= w; j++ ) { //穷举每个重量
            if( j - weight[ i ] < 0 )
                //重量达不到只能不放入
                dp[ i ][ j ] = dp[ i - 1 ][ j ];
            else
                dp[ i ][ j ] = max( dp[ i - 1 ][ j ], dp[ i - 1 ][ j - weight[ i ] ] + value[ i ] );
        }
    }
    cout << dp[ n ][ w ] << endl;
}

             动态规划的初始解法一般都可以优化,对dp数组进行优化,我们根据上面的算法可以得出来dp[i][j]只与它上方的值和左上方的另外一个值相关,我们可以只建立一个一维数组,根据它的记忆性,来不断更新这个一维数组就可以了.

        

        因为dp[i][j]的值会用到其左上的dp[i-1][j-weight[i]]的值,所以我们选择从后边来进行更新。从后往前更新,这样就不会影响到前面的值。

        Code:bottom-up(dp 数组用一维数组来更新)

const int N = 100; //物品个数
const int M = 10000; //背包重量

int value[ N ],weight[ N ]; //物品价值和重量
int dp[ M + 1 ]; //dp数组

//构造dp数组
void knapsack( int n, int w ){//n表示个数,w表示重量
    
    memset( dp, 0, sizeof( dp ) );
    int i, j;
    for ( i = 1; i <= n; i++ ) { //穷举每个物品
        for ( j = w; j - weight[ i ] >= 0; j-- ) { //穷举每个重量
            dp[ j ] = max( dp[ j ], dp[ j - weight[ i ] ] + value[ i ] );
        }
    }
    cout << dp[ w ] << endl;
}


        时间复杂度为o(nw),空间复杂度为o(M),n是物品个数,M为背包的重量限制。

            一般在面试的时候或者ACM题目中,肯定是会选择一维数组这种,二维的空间复杂度太高了,在ACM题目中一般会超出给定内存限制的。

        

        另外一种解法:Code:top-down(从上往下,这个使用的使Memo-ization Algorithm 备忘录法)

#define INF -10000;

const int N = 100; //物品个数
const int M = 10000; //背包重量

int value[ N ],weight[ N ]; //物品价值和重量
int dp[ N + 1 ][ M + 1 ]; //dp数组

//构造dp数组
int knapsack( int n, int w ){//n表示个数,w表示重量
    
    if( w < 0 ) return INF;
    if( n == 0 ) return 0;//没有物品的时候返回0
    
    //Memo-ization,如果值存在直接返回
    if( dp[ n ][ w ] ) return dp[ n ][ w ];
    return dp[ n ][ w ] = max( knapsack( n - 1, w ),
                              knapsack( n - 1, w - weight[ n ] ) + value[ n ] );
}


        POJ练习题目:点击这里(用一维数组,否则会内存超出限制的).


     如何求出背包中放入哪些物品.

        建立一个二维数组,记录每种情况下得问题答案.因为dp[i][j] = max(dp[i-1][j],dp[i-1][j - weight[i]] + value[i]),可以根据值的大小来确定当前情况是由哪个子问题推出来的。如果dp[i-1][j - weight[i]] + value[i] > dp[i-1][j],则表示第i个元素放入到背包中,如果dp[i-1][j]>dp[i-1][j-weight[i]]+value[i]的话,第i个元素不放入背包。放入的情况下设置path[i][j]=1,不放入的情况path[i][j]=0.

        但是这种方式只能找出其中的一种组合方式,因为有dp[j -weigth[i]]+value[i] = dp[j]的情况,这种情况下可以放入也可以不放入当前元素,所以有2种选择,我们是直接设置的不放入。后面会有解法解所有的组合方式。

        Code:
const int N = 100; //物品个数
const int M = 200; //背包重量

int value[ N ],weight[ N ]; //物品价值和重量
int dp[ M + 1 ]; //dp数组
int path[ N + 1 ][ M + 1 ];//记录元素是放还是不放,1表示存放,0表示不存放,初始化为0
//构造dp数组
void knapsack( int n, int w ){//n表示个数,w表示重量
    
    memset( dp, 0, sizeof( dp ) );
    memset( path, 0, sizeof( path ) );
    int i, j;
    for ( i = 1; i <= n; i++ ) { //穷举每个物品
        for ( j = w; j - weight[ i ] >= 0; j-- ) { //穷举每个重量
            //如果满足下面条件则更新,如果不满足则保持不变
            if ( dp[ j - weight[ i ] ] + value[ i ] > dp[ j ] ) {
                dp[ j ] = dp[ j - weight[ i ] ] + value[ i ];
                path[ i ][ j ] = 1; //表示放入背包
            }
        }
    }
    cout << dp[ w ] << endl;
    for ( i = n, j = w; i >= 1; i-- ) {
        if ( path[ i ][ j ] ) {
            cout << " 背包里元素 " << i << endl;
            j -= weight[ i ];
        }
    }
}


        其实我们上面用了一个二维数组计算dp的方法,那个方法可以直接用来得出背包放入了哪些物品,不用在单独定义数组来记录。因为dp[i][j] = max( dp[i-1][j],dp[i-1][j-weight[i]] + value[i]),可以判断在j-weight[i]>=0的情况下,dp[i][j]是否等于dp[i-1][j-weight[i]]+value[i],等于的话代表第i个元素放入包中了。

        Code:
 
        
const int N = 100; //物品个数
const int M = 200; //背包重量

int value[ N ],weight[ N ]; //物品价值和重量
int dp[ N + 1 ][ M + 1 ]; //dp数组

//构造dp数组
void knapsack( int n, int w ){//n表示个数,w表示重量
    
    memset( dp, 0, sizeof( dp ) );
    int i, j;
    for ( i = 1; i <= n; i++ ) { //穷举每个物品,物品编号从1开始
        for ( j = 0; j <= w; j++ ) { //穷举每个重量
            if( j - weight[ i ] < 0 )
                //重量达不到只能不放入
                dp[ i ][ j ] = dp[ i - 1 ][ j ];
            else
                dp[ i ][ j ] = max( dp[ i - 1 ][ j ], dp[ i - 1 ][ j - weight[ i ] ] + value[ i ] );
        }
    }
    cout << dp[ n ][ w ] << endl;
    //反过来推是否
    for ( i = n ,j = w; i >= 1; i-- ) {
        if ( j - weight[ i ] >= 0 && dp[ i ][ j ] == dp[ i - 1 ][ j - weight[ i ] ] + value[ i ] ) {
            cout << "元素" << i << "放入到背包内" << endl;
            j = j - weight[ i ];
        }
    }
}


     如何求背包最大价值的时候里面的物品有哪些不同的组合方式:


        首先想一想为什么会出现这个问题呢。当dp[i][j]=dp[i-1][j]=dp[i][j-weight[i]]+value[i]的时候,就说明此时i这个元素可以放入背包,也可以不放入背包。这就会出现2种选择。我们可以记录下来。记录下来之后然后可以根据回溯法来找到所有组合。

        假设我们用record[n][w]来记录,0代表不把当前节点放入背包,1代表把当前节点放入背包,2表示当前节点可以放入背包,也可以不放入背包。我们需要一个额外数组记录下来当前需要打印的节点。


        Code:
const int N = 100; //物品个数
const int M = 200; //背包重量

int value[ N ],weight[ N ]; //物品价值和重量
int dp[ M + 1 ]; //dp数组
int record[ N + 1 ][ M + 1 ];

int path[ N ];

void find_path( int n, int w, int num ){//n代表第n个元素,w代表背包剩余价值,num代表组合中有几个元素了,用于打印输出判断值的
    static int pathNumber = 1;
    if ( n < 1 ) {
        cout << "This is the " << pathNumber++ << " Path :" << endl;
        for ( int i = 1; i < num; i++ ) {
            cout << path[ i ] << " ";
        }
        cout << endl;
        return;
    }
    if ( record[ n ][ w ] == 0 ){ //不放入背包
        find_path( n - 1, w, num );
    }
    else if ( record[ n ][ w ] == 1 ){
        path[ num ] = n;
        find_path( n - 1, w - weight[ n ], num + 1 );
    }
    else if ( record[ n ][ w ] == 2 ){//可以选择放或者不放,两种选择
        path[ num ] = n;
        find_path( n - 1, w - weight[ n ], num + 1 );
        
        //不放入背包之后调用这个
        find_path( n - 1, w, num );
    }
}
//构造dp数组
void knapsack( int n, int w ){//n表示个数,w表示重量
    
    memset( dp, 0, sizeof( dp ) );
    memset( record, 0, sizeof( record ) );
    int i, j;
    for ( i = 1; i <= n; i++ ) { //穷举每个物品,物品编号从1开始
        for ( j = w; j - weight[ i ] >= 0; j-- ) { //穷举每个重量
            if ( dp[ j - weight[ i ] ] + value[ i ] < dp[ j ]) {
                record[ i ][ j ] = 0;// 第i个元素不放入背包
            }
            else if ( dp[ j - weight[ i ] ] + value[ i ] > dp[ j ] ){
                record[ i ][ j ] = 1; //第i个元素应该放入背包
                dp[ j ] = dp[ j - weight[ i ] ] + value[ i ];
            }
            else{//相等的情况下,可以选择放入也可以选择不放入背包
                record[ i ][ j ] = 2;
            }
        }
    }
    cout << dp[ w ] << endl;
    find_path( n, w, 1 );
}




        



        

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值