动态规划与贪心、分治的区别

贪心算法(Greed alalgorithm) 是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致全局结果是最好或最优的算法。
分治算法(Divide and conquer alalgorithm) 字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
欢迎加群四九九七····五四六一四学习交流,暗号CS。
动态规划算法(Dynamic programming,DP) 通过将原问题分解为相对简单的子问题的方式来求解复杂问题。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
贪心法在处理每个子问题时,不能回退,而动态规划可以保存之前的结果,择优选择。下面针对Interval Scheduling 问题,分析动态规划在实际问题中的应用。

Interval Scheduling 问题
如下图所示,每个长条方块代表一个工作,总有若干个工作a、b… h,横坐标是时间,方块的起点和终点分别代表这个工作的起始时间和结束时间。

当两个工作的工作时间没有交叉,即两个方块不重叠时,表示这两个工作是兼容的(compatible)。

当给每个工作赋权值都为1时,则称为 Unweighted Interval Scheduling 问题;当给每个工作赋不同的正权值时,则称为 Weighted Interval Scheduling 问题。

问题最终是要找到一个工作子集,集合内所有工作权值之和最大且集合内每个工作都兼容。

对于 Unweighted Interval Scheduling 问题,使用贪心算法即可求解,具体做法是按照结束时间对所有工作进行排序,然后从结束最晚的工作开始,依次排除掉与前一个不兼容的工作,剩下的工作所组成的集合即为所求。

然而,对于 Weighted Interval Scheduling 问题,贪心法找到的解可能不是最优的了。此时考虑使用动态规划算法解决问题,兼顾权值选择和兼容关系。

定义P(j)
1、首先依然按照结束时间对所有的工作进行排序;

2、定义p(j)为在工作j之前,且与j兼容的工作的最大标号,通过分析每个工作的起始时间和结束时间,可以很容易计算出p(j);

3、例如下图所示,p(8)=5,因为工作7和6都与8不兼容,工作1到5都与8兼容,而5是其中索引最大的一个,所以p(8)=5。同理,p(7)=3,p(2)=0。

分析递归关系
1、定义opt(j)是j个工作中,所能选择到的最佳方案,即opt(j)是最大的权值和;

2、对于第j个工作,有两种情况:

case 1: 工作j包含在最优解当中,那么往前递推一步,j之前能选择到的最优解是opt(p(j)),即

case 2: 工作j不在最优解中,那么从j个工作中选取解集和从j-1个工作中选取解集是一样的,即

3、当j=0时,显示结果为0,这是边界条件。

后一步的结果取前一步所有可能情况的最大值,因此综上所述,能得到动态规划的递归关系为:

代码实现
1、递归法

递归会使得空间复杂度变高,一般不建议使用。

2、自底向上法

从小到大进行计算,这样每次都可以利用前一步计算好的值来计算后一步的值,算法时间复杂度为O(nlogn),其中排序花费O(nlogn),后面的循环花费O(n)。

Knapsack Problem 问题
背包问题的定义
如下图所示,给定一个背包Knapsack,有若干物品Item
每个item有自己的重量weight,对应一个价值value
背包的总重量限定为W
目标是填充背包,在不超重的情况下,使背包内物品总重量最大。
对于下图的例子,一种常见的贪心思想是:在背包可以装得下的情况下,尽可能选择价值更高的物品。那么当背包容量是W=11时,先选择item5,再选择item2,最后只能放下item1,总价值为28+6+1=35。实际上最优解是选择item3和item4,价值18+22=40。这说明了贪心算法对于背包问题的求解可能不是zuiyou的。下面考虑使用动态规划算法求解,首先要推导递归关系式。

推导递归关系式
类似于Weighted Interval Scheduling问题,定义opt(i, w)表示在有i个item,且背包剩余容量为w时所能得到的最大价值和。

考虑第i个item,有选和不选两种情况:

case 1: 如果选择第i个item,则

case 2: 如果不选择第i个item,则

边界条件: 当i=0时,显然opt(i,w)=0。

后一步的结果取前一步所有可能情况的最大值,因此综上所述,能得到动态规划的递归关系为:

自底向上求解

算法迭代过程如下表:

算法运行时间分析

值得注意的是,该算法相对于输入尺寸来说,不是一个多项式算法,虽然O(nW)看起来很像一个多项式解,背包问题实际上是一个NP完全问题。

为了便于理解,可以写成这种形式:

W在计算机中只是一个数字,以长度logW的空间存储,非常小。但是在实际运算中,随着W的改变,需要计算nW次,这是非常大的(相对于logW来说)。例如,当W为5kg的时候,以kg为基准单位,需要计算O(5n)次,当W为5t时,仍然以kg为单位,需要计算O(5000n)次,而在计算机中W的变化量相对很小。

Sequence Alignment
Define edit distance
给定两个序列x1,x2…xi和y1,y2,…,yj。要匹配这两个序列,使相似度足够大。首先需要定义一个表示代价的量-Edit distance,只有优化使这个量最小,就相当于最大化匹配了这两个序列。

Edit distance的定义如下所示。

其中,匹配到空,设距离为delta,否则字母p和q匹配的距离记为alpha(p,q),如果p=q,则alpha=0;

那么两个序列匹配的总代价为:

建立递推关系
设opt(i,j)是序列x1,x2…xi和y1,y2,…,yj之间匹配所花费的最小代价。当i,j不全为0时,则分别有三种情况,分别是xi-gap,yj-gap,xi-yj,分别计算不同匹配情况所花费的代价,再加上前一步的结果,就可以建立递推关系式,如下所示。

算法实现

算法复杂度
时间和空间复杂度皆为O(mn)。

下面再分析一个具体的编程问题,使用动态规划算法,但是和上面的DP又有一些区别。

合唱团问题
问题定义
有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?

输入描述

每个输入包含 1 个测试用例。每个测试数据的第一行包含一个整数 n (1 <= n <= 50),表示学生的个数,接下来的一行,包含 n 个整数,按顺序表示每个学生的能力值 ai(-50 <= ai <= 50)。接下来的一行包含两个整数,k 和 d (1 <= k <= 10, 1 <= d <= 50)。

输出描述

输出一行表示最大的乘积。

问题分析
此题的第一个关键点是“要求相邻两个学生的位置编号的差不超过 d”,如果按照传统的DP思路,定义opt(i,k)表示在前i个学生中选取k个学生的最大乘积,建立递推关系:

则无法实现“相邻两个学生的位置编号的差不超过 d”的要求。因此,需要定义一个辅助量,来包含对当前学生的定位信息。

定义f(i,k)表示在前i个学生中选取k个学生,且第i个学生必选时,所选学生的能力值乘积,这样就包含对当前学生的定位信息,f的递推关系可以表示为

其中,j是一个比i小的值,最大为i-1,i、j之差不超过D,f(j,k-1)表示在前j个学生中,选择k-1个学生,且第j个学生必选。f(i,k)选择了第i个学生,f(j,k-1)选择了第j个学生,i、j之差不超过D,这样就可以满足题目要求了。

辅助量f(i,k)并不是我们最终要得到的结果,最终结果opt(i,k)表示在前i个学生中选取k个学生的最大乘积,因此,可以得到opt(i,k)和f(i,k)的关系为:

该问题的第二个关键点是学生的能力值在-50到+50之间,每次选择的学生的能力值有正有负,所以需要两个f记录最大和最小值,定义fmax和fmin,在每次迭代f的过程中:

当k=K,i=N时,最终所求的:

边界条件k=1时,f(i,k=1)=v(i)
代码实现
/***********************************************************
*
* Ran Chen [email protected]
*
* Dynamic programming algorithm
*
***********************************************************/

include

include

include

include

using namespace std;

int main()
{
int N, D, K; // 总共N个学生
vector value;

while (cin >> N)
{

    for (int i = 0; i < N; ++i)
    {
        int v;
        cin >> v;
        value.push_back(v);
    }

    break;
}

cin >> K;  // 选择K个学生
cin >> D;  // 相邻被选择学生的序号差值

// fmax/fmin[i, k]表示在选择第i个数的情况下的最大/小乘积
vector <vector <long long>> fmax(N+1, vector <long long> (K+1));
vector <vector <long long>> fmin(N+1, vector <long long> (K+1));

// 边界条件k=1
for (int i = 1; i <= N; ++i)
{
    fmax[i][1] = value[i - 1];
    fmin[i][1] = value[i - 1];
}

// 自底向上dp, k>=1
for (int k = 2; k <= K; ++k)
{
    // i >= k
    for (int i = k; i <= N; ++i)
    {
        // 0 <= j <= i-1 && i - j <= D && j >= k-1
        long long *max_j = new long long; *max_j = LLONG_MIN;
        long long *min_j = new long long; *min_j = LLONG_MAX;

        // f(i, k) = max_j {f(j, k-1) * value(i)}
        int j = max(i - D, max(k - 1, 1));
        for ( ; j <= i - 1; ++j)
        {
            *max_j = max(*max_j, max(fmax[j][k - 1] * value[i - 1], fmin[j][k - 1] * value[i - 1]));                
            *min_j = min(*min_j, min(fmax[j][k - 1] * value[i - 1], fmin[j][k - 1] * value[i - 1]));            
        }

        fmax[i][k] = *max_j;
        fmin[i][k] = *min_j;

        delete max_j; 
        delete min_j;
    }
}

// opt(N, K) = max_i {f(i, K)}, K <= i <= N
long long *temp = new long long;
*temp = fmax[K][K];
for (int i = K+1; i <= N; ++i)
{
    *temp = max(*temp, fmax[i][K]);
}
cout << *temp;
delete temp;

system("pause");
return 0;

}

猜你喜欢

转载自blog.csdn.net/bestyjava/article/details/82352072