动态规划专题

Posted 雪伦_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划专题相关的知识,希望对你有一定的参考价值。

1.0-1背包

设计一个动态规划算法,通常可按照以下几个步骤进行:
(1) 找出最优解的性质,并刻画其结构特征。
(2) 递归地定义最优解的值
(3) 以自底而上的方式计算出最优值
(4) 根据计算最优值时得到的信息,构造一个最优解。

对于一个给定的问题,若具有以下两个性质,则可以考虑用动态规划法来求解。

(1) 最优子结构。如果一个问题的最优解中包含了其子问题的最优解,就说该问题具有最优子结构。当一个问题具有最优子结构时,提示我们动态规划法可能会适用,但是此时贪心策略可能也是适用的。

(2) 重叠子问题。指用来解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。即当一个递归算法不断地调用同一个问题时,就说该问题包含重叠子问题。此时若用分治法递归求解,则每次遇到子问题都会视为新问题,会极大地降低算法的效率,而动态规划法总是充分利用重叠子问题,对于每个子问题仅计算一次,把解保存在一个在需要时就可以查看的表中,而每次查表的时间为常数。

问题:有n个物品,第i个物品价值为vi,重量为wi,其中vi和wi均为非负数,背包的容量为W,W为非负数。现需要考虑如何选择装入背包的物品,使装入背包的物品总价值最大。该问题以形式化描述如下:

目标函数为:

约束条件为:

满足约束条件的任一集合(x1,x2,…,xn)是问题的一个可行解,问题的目标是要求问题的一个最优解。考虑一个实例,假设n=5,W=17, 每个物品的价值和重量如表9-1所示。可将物品1,2和5装入背包,背包未满,获得价值22,此时问题解为你(1,1,0,0,1)。也可以将物品4和5装入背包,背包装满,获得价值24,此时解为(0,0,0,1,1)。

下面根据动态规划的4个步骤求解该问题。

(1) 刻画0-1背包问题的最优解的结构。

可以将背包问题的求解过程看作是进行一系列的决策过程,即决定哪些物品应该放入背包,哪些物品不放入背包。如果一个问题的最优解包含了物品n,即xn=1,那么其余x1,x2,…,x(n-1)一定构成子问题1,2,…,n-1在容量W-wn时的最优解。如果这个最优解不包含物品n,即xn=0,那么其余x1,x2,…,x(n-1)一定构成子问题1,2,…,n-1在容量W时的最优解。

(2)递归定义最优解的值

根据上述分析的最优解的结构递归地定义问题最优解。设c[i,w]表示背包容量为w时,i个物品导致的最优解的总价值,得到下式。显然要求c[n,w]。

(3)计算背包问题最优解的值

(4)根据计算的结果,构造问题最优解。

根据上一步计算的c数组,很容易构造问题的最优解。判断c[i,w]与c[i-1,w]的值是否相等,若相等,则说明xi=0,否则为1。

#include <iostream>
#include <vector>

using namespace std;

void output_vec(vector<int> vec)
{
    if(vec.empty())
        return;
    else
    {
        for (int i = 0; i < vec.size(); i++)
        {
            cout << vec[i] << " ";
        }
        cout << endl;
    }
}

//使用动态规划得到递归解的值
void DP(vector<vector<int>>& cost, const int N, const int W, const vector<int> weight, const vector<int> value)
{
    for (int i = 1; i <= N; i++)
    {
        cost[i][0] =  0;
        for (int w = 1; w <= W; w++)
        {
            if (weight[i-1] > w)
            {
                cost[i][w] = cost[i-1][w];
            } 
            else
            {
                int tmp = cost[i-1][w -weight[i-1]] + value[i-1];
                if(cost[i-1][w] > tmp)
                    cost[i][w] = cost[i-1][w];
                else
                    cost[i][w] = tmp;
            }
        }
    }
}


//自下而上得到result
void findPath(vector<vector<int>> cost, int N, int W, vector<int> weight, vector<int>& result)
{
    int w = W; //用于记录重量

    for (int i = N; i >= 2; i--)
    {
        if (cost[i][w] == cost[i-1][w])
        {
            result[i-1] = 0;
        } 
        else
        {
            result[i-1] = 1;
            w -= weight[i-1];
        }
    }

    if(cost[1][w] == 0)
        result[0] = 0;
    else
        result[0] = 1;
}

int main()
{
    int N = 5;   //物品数
    int W = 17;  //最多可以容纳的重量
    int w[] = {3, 4, 7, 8, 9};    //每个物品的重量
    int v[] = {4, 5, 10, 11, 13}; //每个物品的价值量

    vector<int> weight(w,w+N);   //利用数组给vector赋值
    vector<int> value(v,v+N);

    vector<vector<int>> cost(N+1);  //加1是为了cost下标可以从1开始
    for (int i = 0; i < N+1; i++)
        cost[i].resize(W+1);

    DP(cost,N,W,weight,value);

    vector<int> result(N);

    findPath(cost,N,W,weight,result);

    cout << "N : " << N << endl;

    cout << "W : " << W << endl;

    cout << "weight : ";
    output_vec(weight);

    cout << "value : ";
    output_vec(value);

    cout << "Result : ";
    output_vec(result);

    return 0;
}

2.旅行商问题

2.1 问题定义

TSP问题(旅行商问题)是指旅行家要旅行n个城市,要求各个城市经历且仅经历一次然后回到出发城市,并要求所走的路程最短。

假设现在有四个城市,0,1,2,3,他们之间的代价如图一,可以存成二维表的形式

现在要从城市0出发,最后又回到0,期间1,2,3都必须并且只能经过一次,使代价最小。

2.2 动态规划可行性

设s, s1, s2, …, sp, s是从s出发的一条路径长度最短的简单回路,假设从s到下一个城市s1已经求出,则问题转化为求从s1到s的最短路径,显然s1, s2, …, sp, s一定构成一条从s1到s的最短路径,所以TSP问题是构成最优子结构性质的,用动态规划来求解也是合理的。

2.3 推导动态规划方程

假设从顶点s出发,令d(i, V’)表示从顶点i出发经过V’(是一个点的集合)中各个顶点一次且仅一次,最后回到出发点s的最短路径长度。

推导:(分情况来讨论)

①当V’为空集,那么d(i, V’),表示从i不经过任何点就回到s了,如上图的 城市3->城市0(0为起点城市)。此时d(i, V’)=Cis(就是 城市i 到 城市s 的距离)、

②如果V’不为空,那么就是对子问题的最优求解。你必须在V’这个城市集合中,尝试每一个,并求出最优解。

d(i, V’)=min{Cik + d(k, V’-{k})}

注:Cik表示你选择的城市和城市i的距离,d(k, V’-{k})是一个子问题。

综上所述,TSP问题的动态规划方程就出来了:

2.4 实例分析

现在对问题定义中的例子来说明TSP的求解过程。(假设出发城市是 0城市)

①我们要求的最终结果是d(0,{1,2,3}),它表示,从城市0开始,经过{1,2,3}之中的城市并且只有一次,求出最短路径.

②d(0,{1,2,3})是不能一下子求出来的,那么他的值是怎么得出的呢?看上图的第二层,第二层表明了d(0,{1,2,3})所需依赖的值。那么得出:

d(0,{1,2,3})=min  {
        C01+d(1,{2,3})
        C02+d{2,{1,3}}
        C03+d{3,{1,2}}
}

③d(1,{2,3}),d(2,{1,3}),d(3,{1,2})同样也不是一步就能求出来的,它们的解一样需要有依赖,就比如说d(1,{2,3})

d(1,{2,3})=min{
     C12+d(2,{3})                             
     C13+d(3,{2})
}

d(2,{1,3}),d(3,{1,2})同样需要这么求。

④按照上面的思路,只有最后一层的,当当V’为空集时,Cis的值才可以求,它的值是直接从下图中获得

2.5 编程思路

下图中,将d(i, V’)转换成二维表,d[i][j]

2.6 完整代码

代码空间复杂度可以继续优化,感兴趣的读者可以尝试一下。

#include <iostream>
#include <set>
#include <vector>

using namespace std;


//测试用例1
//#define MAX 6
//
//int dis[MAX][MAX]={
//  0, 10, 20, 30, 40, 50,
//  12, 0 ,18, 30, 25, 21,
//  23, 19, 0, 5,  10, 15,
//  34, 32, 4, 0,  8,  16,
//  45, 27, 11,10, 0,  18,
//  56, 22, 16,20, 12,  0
//};

//测试用例2
#define MAX 4

int dis[MAX][MAX] = {
    0, 3, 6, 7,
    5, 0, 2, 3,
    6, 4, 0, 2,
    3, 7, 5, 0
};

struct status{
    int cur_city; //当前所在的城市
    vector<int> unvisited_city; //当前未访问的城市
    set<int> type; //由于set自动排序,相同状态的vector可能不同,但set必然相同
    int distance; //从当前城市到终点回到起点的距离
};

//测试用
void output_vec(vector<status> vec)
{
    vector<status>::iterator status_iter;
    vector<int>::iterator iter;
    for (status_iter = vec.begin(); status_iter != vec.end(); status_iter++)
    {
        cout << (*status_iter).cur_city << "<";
        for (iter = (*status_iter).unvisited_city.begin(); iter != (*status_iter).unvisited_city.end(); iter++)
        {
            cout << *iter << " ";
        }
        cout << "> distance : " << (*status_iter).distance << endl;
    }
}

//查看当前状态的城市中是否包含城市i
bool contain(int city, status& sta)
{
    vector<int>::iterator iter;

    if (city == sta.cur_city)
    {
        return true;
    } 
    else
    {
        for (iter = sta.unvisited_city.begin(); iter != sta.unvisited_city.end(); iter++)
        {
            if(city == *iter)
                return true;
        }
    }

    return false;
}

//合并相同状态,留下其中distance比较小的
vector<status> combine_status(vector<status> vec)
{
    vector<status> new_vec;
    vector<status>::iterator iter;
    status tmp;

    while (vec.size() > 0)
    {
        iter = vec.begin();
        tmp = *iter;
        iter = vec.erase(iter); 

        /*
        C++ vector erase
        如果是删除指定位置的元素时:返回值是一个迭代器,指向删除元素下一个元素;
        如果是删除某范围内的元素时:返回值也表示一个迭代器,指向最后一个删除元素的下一个元素;
        */

        while (iter != vec.end())
        {
            if(tmp.cur_city == (*iter).cur_city && tmp.type == (*iter).type)
            {
                if((*iter).distance < tmp.distance)
                    tmp = *iter;
                iter =  vec.erase(iter);   //当vec.size比较大时,可以节约时间
            }
            else
                iter++;
        }

        new_vec.push_back(tmp);
    }

    return new_vec;
}

vector<status> DP()
{
    vector<status> pre_vec;
    vector<status> cur_vec;
    int i,j;

    //从后往前推,初始化
    for (int i = 1; i < MAX; i++)   //i= 0默认为出发城市
    {
        status sta;
        sta.cur_city = i;
        sta.distance = dis[i][0];

        cur_vec.push_back(sta);
    }

    //依次递推,递推MAX-2次
    for (j = 0; j < MAX-2; j++)
    {
        pre_vec = cur_vec;
        cur_vec.clear();

        for (i = 1; i < MAX; i++)
        {
            vector<status>::iterator iter;
            for (iter = pre_vec.begin(); iter != pre_vec.end(); iter++)
            {
                status tmp = *iter;
                if(contain(i,tmp) == false) //确保状态中没有重复路径
                {
                    status new_sta = tmp;
                    vector<int>::iterator int_iter = new_sta.unvisited_city.begin();

                    new_sta.unvisited_city.insert(int_iter,new_sta.cur_city);
                    new_sta.type.insert(new_sta.cur_city);
                    new_sta.distance += dis[i][new_sta.cur_city];
                    new_sta.cur_city = i;

                    cur_vec.push_back(new_sta);
                }
            }
        }

        //记录相同状态最短路径,并合并相同状态
        cur_vec = combine_status(cur_vec);
    }

    return cur_vec;
}

int main()
{
    vector<status> cur_vec = DP();

    //output_vec(cur_vec);

    //递推完毕后,最后一步,计算起点到每个状态的距离,找到最短路径
    vector<status>::iterator iter = cur_vec.begin();
    status shortest = *iter;
    int min_dis = shortest.distance += dis[0][shortest.cur_city];   //得到最小距离
    iter++;
    for (; iter != cur_vec.end(); iter++)
    {
        int tmp_dis = dis[0][(*iter).cur_city] + (*iter).distance;
        if(tmp_dis < min_dis)
        {
            shortest = *iter;
        }
    }

    //打印结果
    cout << "Min distance is : " << min_dis << endl;

    //路径
    cout << "The shortest path is : " << "1 " << shortest.cur_city+1; 
    for (vector<int>::iterator iter_city = shortest.unvisited_city.begin(); iter_city != shortest.unvisited_city.end(); iter_city++)
        cout << " " << (*iter_city + 1);
    cout << " 1" << endl;

    return 0;
}

3.连续子数组最大的和

充分利用了题目的特性。算法时间复杂度O(n)。

int max_sub_array(int a[],int low, int high)
{
    int sum = numeric_limits<int>::min(); //定义无穷小
    int cur_sum = 0;

    for(int i = low; i <= high; i++)
    {
        if (cur_sum > 0)
        {
            cur_sum += a[i];
        }
        else
        {
            cur_sum = a[i];
        }

        if (cur_sum > sum)
            sum = cur_sum;
    }

    return sum;
}

4.Minimum Path Sum

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

分析:设状态为f[i][j],表示从起点(0,0)到达(i,j)的最小路径和,则状态转移方程为f[i][j] = min(f[i-1][j],f[i][j-1]) + grid[i][j]

Solution:

int minPathSum(vector<vector<int>> &grid)
{
    if(grid.size() == 0)
        return 0;

    const int m = grid.size();    //行
    const int n = grid[0].size(); //列
    int i,j;

    vector<vector<int>> f(m);
    for(i = 0; i < m; i++)
        f[i].resize(n);


    f[0][0]  = grid[0][0];

    for(i = 0; i < m; i++)
        f[i][0] = f[i-1][0] + grid[i][0];

    for (i = 0; i < n; i++)
        f[0][i] = f[0][i-1] + grid[0][i];

    for(i = 1; i < m; i++)
    {
        for(j = 1; j < n; j++)
        {
            f[i][j] = min(f[i-1][j], f[i][j-1]) + grid[i][j];
        }
    }

    return f[m-1][n-1];
}

5.Maximal Rectangle

Given a 2D binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area.

For example, given the following matrix:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Return 6.

Solution:

int maxRect(vector<vector<char>>& matrix)
{
    if(matrix.size() == 0)
        return 0;

    const int m = matrix.size();
    const int n = matrix[0].size();
    vector<int> H(n,0); //高
    vector<int> L(n,0); //左
    vector<int> R(n,n); //右

    int ret = 0;
    int i,j;

    for (i = 0; i < m; i++)
    {
        int left = 0,right = n;

        //calculate L(i,j) from left to right
        for (j = 0; j < n; j++)
        {
            if(matrix[i][j] == '1')
            {
                H[j]++;
                L[j] = max(L[j],left);
            }
            else
            {
                left = j+1;
                H[j] = 0;
                L[j] = 0;
                R[j] = n;
            }
        }

        //calculate R[i,j] from right to left
        for(j = n-1; j >= 0; j--)
        {
            if(matrix[i][j] == '1')
            {
                R[j] = min(R[j],right);
                ret = max(ret, H[j]*(R[j]-L[j]));
            }
            else
            {
                right = j;
            }
        }
    }

    return ret;
}

6.Best Time to Buy and Sell Stock

Say you have an array for which the ith element is the price of a given stock on day i.

If you were only permitted to complete at most one transaction (ie, buy one and sell one share of the stock), design an algorithm to find the maximum profit.

Example 1:

Input: [7, 1, 5, 3, 6, 4]
Output: 5

max. difference = 6-1 = 5 (not 7-1 = 6, as selling price needs to be larger than buying price)

Example 2:

Input: [7, 6, 4, 3, 1]
Output: 0

In this case, no transaction is done, i.e. max profit = 0.

Solution:

   int maxProfit(vector<int>& prices) 
   {  
       if(prices.size()<=1)  
           return 0;  
       int curMin=prices[0];  
       int maxProfit=0;  
       for(int i=1;i<prices.size();i++)  
       {  
           maxProfit=max(maxProfit,prices[i]-curMin);  
           curMin=min(curMin,prices[i]);//获得历史最小价格的股票  
       }  
       return maxProfit;  
   }  

7.Best Time to Buy and Sell Stock III

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete at most two transactions.

Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

分析:

Solution:

int maxProfit(vector<int>& price)
{
    if(price.size() < 2)
        return 0;

    const int n = price.size();
    int i;
    vector<int> f(n,0);
    vector<int> g(n,0);

    int valley = price[0];
    for(i = 1; i < n; i++)
    {
        valley = min(valley, price[i]);
        f[i] = max(f[i-1], price[i]-valley);
    }

    int peak = price[n-1];
    for (i = n-2; i >=0; i--)
    {
        peak = max(peak, price[i]);
        g[i] = max(g[i], peak-price[i]);
    }

    int max_profit = 0;
    for (i = 0; i < n; i++)
        max_profit = max(max_profit, f[i]+g[i]);

    return max_profit;
}

8.Word Break

Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

For example, given
s = “leetcode”,
dict = [“leet”, “code”].

Return true because “leetcode” can be segmented as “leet code”.

分析:

Solution:

时间复杂度O(n^2),空间复杂度O(n)

bool wordBreak(string s, unordered_set<string>& dict)
{
    //长度为n的字符串有n+1个隔板
    vector<bool> f(s.size()+1, false);
    f[0]  = true;
    for (int i = 1; i <= s.size(); i++)
    {
        for (int j = i-1; j >= 0; j--)
        {
            if (f[j] && dict.find(s.substr(j,i-j)) != dict.end())
            {
                f[i] = true;
                break;
            }
        }
    }
    return f[s.size()];
}

9.Word Break II

Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.

Return all such possible sentences.

For example, given
s = “catsanddog”,
dict = [“cat”, “cats”, “and”, “sand”, “dog”].

A solution is [“cats and dog”, “cat sand dog”].

Solution:

时间复杂度O(n^2),空间复杂度O(n^2)

vector<string> wordBreak(string s, unordered_set<string>& dict)
{
    //长度为n的字符串有n+1个隔板
    vector<bool> f(s.length()+1, false);

    //prev[i][j]为true,表示s[j,i)是一个合法单词,可以从j处分开
    //第一行未用
    vector<vector<bool>> prev(s.length()+1, vector<bool>(s.length()));
    f[0] = true;
    int i,j;

    for (i = 1; i <= s.length(); i++)
    {
        for (j = i-1; j >=0; j--)
        {
            if(f[j] && dict.find(s.substr(j,i-j) != dict.end()))
            {
                f[i] = true;
                prev[i][j] = true;
            }
        }
    }

    vector<string> result;
    vector<string> path;
    gen_path(s, prev, s.length(), path, result);
    return result;
}

//DFS遍历树,生成路径
void gen_path(const string &s, const vector<vector<bool>>& prev, int cur, vector<string>& path, vector<string>& result)
{
    if (cur == 0)
    {
        string tmp;
        vector<string>::iterator iter;
        for(iter = path.begin(); iter != path.end(); iter++)
            tmp += *iter + " ";
        tmp.erase(tmp.end()-1);
        result.push_back(tmp);
    } 
    else
    {
        for (int i = 0; i < s.size(); i++)
        {
            if (prev[cur][i])
            {
                path.push_back(s.substr(i,cur-i));
                gen_path(s,prev,i,path,result);
                path.pop_back();
            }
        }
    }
}

以上是关于动态规划专题的主要内容,如果未能解决你的问题,请参考以下文章

动态规划专题1:斐波拉契数列问题的递归和动态规划

poj 动态规划专题练习

动态规划入门专题合集

算法面试专题-动态规划

#yyds干货盘点# 动态规划专题:小红取数

动态规划专题2:矩阵的最小路径和