又一个周末
动态规划与贪婪算法
使用动态规划的情况
1、使用动态规划的情况:求最优解(最大值或最小值)的问题,并且要求这个问题能分解成若干个子问题,并且子问题之间还有重叠的更小的子问题。
2、在应用动态规划之前要分析能否把大问题分解成小问题,分解后的每个小问题也存在最优解,之后把小问题的最优解组合起来能够得到整个问题的最优解,就用动态规划解决这个问题
动态规划的特点
1、求最优解
2、整体问题的最优解是依赖各个子问题的最优解
3、把大问题分解成若干个小问题,这些小问题之间还有相互重叠的更小的子问题
4、由于子问题在分解大问题的过程中重复出现,为了避免重复求解子问题,可以用从下往上的顺序计算小问题的最优解并存储下来,再以此为基础求取大问题的最优解。从上往下分析问题,从下往上解决问题。在应用动态规划解决问题的时候,要从解决最小问题开始,并把已经解决的子问题的最优解存储下来【在一维数组或二维数组中】,并把子问题的最优解组合起来逐步解决大问题。
贪婪算法:每步都采取最优解,最终得到局部的最优解
题目
剪绳子
给你一根长度为n的绳子,请把绳子剪成m段(m和n都是int,n>1并且m>1),每段绳子的长度记为k[0],k[1]……k[m]。请问k[0]*k[1]*……*k[m]
可能的最大乘积是多少?
动态规划:时间O( n2 n 2 ),空间O(n)
f(n)=max(f(i)∗f(n−i)),0<i<n
f
(
n
)
=
m
a
x
(
f
(
i
)
∗
f
(
n
−
i
)
)
,
0
<
i
<
n
,这是一个至上而下的递归公式,如果用递归的话会需要重复计算很多子问题,所以我们就按照自下而上来进行计算,先得到
f(2),f(3)
f
(
2
)
,
f
(
3
)
,接着
f(4),f(5)
f
(
4
)
,
f
(
5
)
,直到计算到
f(n)
f
(
n
)
时间O(
n2
n
2
),是因为有双重循环;空间O(n)是因为使用了一个新的长为n+1的数组
//f(2)=1*1;f(3)=1*2>1*1*1?1*2:1*1*1
int maxProductAfterCutting1(int length)
{
if (length < 2)//0||1
return 0;
if (length == 2)
return 1;
if (length == 3)
return 2;
//为什么是n+1?因为要计算的是第n个值,在数组中有第n个值的数组,最少要n+1长(从第0开始算)
int *products = new int[length + 1];
int i;
for (i = 0; i <= 3; i++)
products[i] = i;
int max = 0;
//i=4,下面的算法类似于斐波那契数列
for (; i <= length; i++) {
max = 0;
for (int j = 1; j <= i / 2; j++) {
int product = products[j] * products[i - j];
if (max < product)
max = product;
products[i] = max;
}
}
max = products[length];
delete[] products;
return max;
}
贪婪算法:时间和空间O(1)
剪绳子的时候,按照下面策略剪会使得到的各段绳子的长度的乘积最大(这要用数学方法进行证明):当 n>=5 n >= 5 时,尽可能多的剪长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。
//贪婪算法
int maxProductAfterCutting2(int length)
{
if (length < 2)//0||1
return 0;
if (length == 2)
return 1;
if (length == 3)
return 2;
//尽可能剪去长度为3的绳子
int timeOf3 = length / 3;
//当绳子最后剩下的长度为4时,不再剪去3,而是2*2;只有4才需要-1
if (length - timeOf3 * 3 == 1)//表示最后剩下的是3+1=4
timeOf3--;
//因为倒数两段必须>=5才能继续剪3,所以倒数最后一段只能为4,3,2;
//如果最后一段是4,则2;3,则0,但timeOf3没有减1;2,则1,反正最后也是等于2;
//最后一段是1,2,3就不需要再剪了,保持就好
int timeOf2 = (length - timeOf3 * 3) / 2;
return (int)(pow(3, timeOf3))*(int)(pow(2, timeOf2));
}
抽象建模能力的锻炼
把现实问题抽象成数学模型并用计算机的编程语言表达出来:选定数据结构→分析内在规律(列出公式,使用各种数学方法)
扑克牌中的顺子
从扑克牌中随机抽出5张牌,判断是不是顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大王、小王可以看成任意数字。
思路
1、将大王、小王抽象为0
2、将抽出来的5个数进行排序
3、统计数组中0的个数
4、判断是否有对子,有对子则一定不是顺子
5、统计排序之后的数组中相邻数字之间的空缺总数,判断孔雀的总数是否小于或等于0的个数
#include<stdlib.h> //qsort
int compare(const void* arg1, const void* arg2)
{
return *(int*)arg1 - *(int*)arg2;
}
bool isContinuous(int *numbers, int length)
{
//先排除不按理出牌的情况
if (numbers == nullptr || length < 1)
return false;
//void qsort (void* base, size_t num, size_t size,int(*compar)(const void*, const void*));
/*这个函数被qsort重复调用来比较两个元素。 它应遵循以下原型:int compar (const void* p1, const void* p2);
时间复杂度O(nlogn)*/
qsort(numbers, length, sizeof(int), compare);
int numberOfZero = 0;
int numberOfGap = 0;
//统计0的个数,这是在排好序后再统计的,所以0是连在一起的
for (int i = 0; i < length&&numbers[i] == 0; i++)
numberOfZero++;
//统计数组中的间隔数目,small是前面那个数的坐标,big是后面那个数的坐标
int small = numberOfZero;
int big = small + 1;
while (big < length) {
//两个数相等,有对子,所以就直接over了
if (numbers[small] == numbers[big])
return false;
numberOfGap += numbers[big] - numbers[small] - 1;
small++;
big++;
}
return (numberOfGap <= numberOfZero);
}
股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?例如,一只股票在某些时间节点的价格为{9,11,8,5,7,12,16,14}。如果我们能在价格为5的时候买入并在价格为16时卖出,则能收获最大利润为11
思路:
1、数据结构:数组
2、股票交易的利润来自股票买入和卖出的差价。只能在买入之后才能卖出,所以把股票的买入和卖出两个价组成数对,利润就是数对的差值。所以最大的利润就是其中某一个数对的差值。
3、蛮力法:找出数组中所有数对,并计算它们的差值。由于长度为n的数组中存在O(
n2
n
2
)个数对,所以算法复杂度也就有O(
n2
n
2
)
4、更好的方法:在卖出价固定时,买入价越低获得的利润越大。也就是说,如果扫描到数组中的第i个数字时,只要我们能够记住之前的i-1个数字的最小值,就能算出在当前价位卖出时能得到的最大利润。所以i也要不断变化
//时间复杂度O(n),不用说组所有的数对,因为现实问题也只能用前面的当买入价,后面的当卖出价
//关于股票的买卖问题
int MaxDiff(const int* numbers, unsigned length)
{
if (numbers == nullptr || length < 2)
return 0;
int min = numbers[0];
int maxDiff = numbers[1] - min;
//每个i都当一次卖出价,每一轮中i前面的又都当一次买入价
for (int i = 2; i < length; i++)
{
//因为i增加1,那它前面的数也只增加了1个就是i-1
if (numbers[i - 1] < min)
min = numbers[i - 1];
int currentDiff = numbers[i] - min;
if (currentDiff > maxDiff)
maxDiff = currentDiff;
}
return maxDiff;
}
记得前阵子笔试的时候有一道这道题的拓展题,但是现在找不到原题了,(⊙﹏⊙)