实验三:动态规划算法与贪心算法
用动态规划思想设计实现最长公共子序列问题,用贪心思想设计实现活动安排问题,并且用不同数据量进行实验对比分析,要求分析算法的时间复杂性并且形成分析报告。
- 问题描述
(1)动态规划算法
动态规划算法, Dynamic Programming简称DP,通常基于一个递推公式及一个或多个初始状态。 当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度, 因此它比回溯法、暴力法等要快许多。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
(2)最长公共子序列问题(LCS)
最长公共子序列(LCS)是一个在一个序列集合中(通常为两个序列)用来查找所有序列中最长子序列的问题。一个数列 ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则称为已知序列的最长公共子序列。 [1]
最长公共子序列问题是一个经典的计算机科学问题,也是数据比较程序,比如Diff工具,和生物信息学应用的基础。它也被广泛地应用在版本控制,比如Git用来调和文件之间的改变。
(3)贪心算法
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关。)
(4)活动安排问题
有 n 个活动需要使用同一资源,且同一时段内只有一个活动能使用该资源。每个活动 i 都有起始时间 si 和结束时间 ei (si < ei),如果安排活动 i,则它在时间区间 [si,ei) 内占用资源。若区间 [si, ei) 与区间 [sj,ej) 不相交,则称活动 i 与活动 j 相容。
活动安排问题即,安排活动使尽可能多的活动相容。本质为,在所给的活动集合中选出最大的相容活动子集合。
- 实验目的
加深对动态规划思想设计方法和贪心思想设计方法基本思想的理解。
- 实验原理
描述分治法思想及如何用分治算法解决问题,包括复杂性分析。
- 最长公共子序列问题
一个给定序列的子序列是在该序列中删去若干元素得到的序列。
同时是多个序列的子序列的序列,就是这多个序列的公共子序列。
而最长公共子序列问题限制在两个序列中。
①最优子结构性质:最长公共子序列具有最优子结构性质。这带来一个递归进行的方法:
对于X={x1,x2,x3,…,xm},Y={y1,y2,y3,…,ym}:
Xm=yn时,找出对应位数减一的最长公共子序列,在后面加上xm或者yn,就是两个序列的最长公共子序列。
Xm!=yn时,必须解两个子问题,即找出:
Xm-1,Y的最长公共子序列
X,Yn-1的最长公共子序列
进行比较取最长即可。
②解法:
数据结构:设立两个二维数组,一个用来递归求子序列,一个用来记录求值方法。
函数:LCSLength用来计算最优值,LCS用来打印子序列。
时间复杂度:O(mn)+O(m+n)
(2)活动安排问题
活动安排问题有很多可能的数据输入形式,我这里用二维数组(列表)的形式输入。嵌套的列表,子列表的第一个数是活动开始时间,第二个是结束时间。不按顺序给出列表。
解法的思想是贪心算法思想的体现,即逐个计算开始时间与第一个时间的差值。将新的活动加入。并将新的活动的结束时间作为标准。直到遍历到最后一个活动。
时间复杂度:排序的时间复杂度另说,活动安排函数本身的时间复杂度是O(n)
- 实验设计
4.1 最长公共子串
LCS.c由3个函数组成:
它们的关系如下:
4.2 活动安排
由排序函数,活动安排函数,主函数(包括随机数生成部分)组成:
它们的关系如下:
- 实验结果与分析
5.1 最长公共子序列
我以书上 p54 的例子运行程序,运行结果如下:
这点数据显然是不够做测试的。因此我决定加大数据量。
我为这个文件增加了一个产生随机字符串的函数。函数原理和之前的差不多,只不过这次是将大小写字母和阿拉伯数字放在一个字符串里,一共62个。然后产生随机数将字母提取出来拼接到字符串里。
字符串数据个数 | LCSLength时间(ms) | LCS时间(ms) |
50 | 0 | 0 |
100 | 0 | 0 |
150 | 0 | 0 |
200 | 0 | 0 |
250 | 0 | 0 |
300 | 0 | 0 |
350 | 0 | 0 |
400 | 0 | 0 |
450 | 0 | 0 |
500 | 1 | 0 |
以下为50个字符时的测试截图:
以下为500个字符时的测试截图:
600个字符及以后的程序会产生指针越界或内存溢出错误:
5.2 活动安排
排序的时间没有意义(而且我用的是冒泡排序,时间比较长),所以重点在活动安排的时间:
活动个数 | 活动安排时间(ns) |
100 | 0 |
200 | 0 |
300 | 0 |
400 | 0 |
500 | 0 |
600 | 0 |
700 | 0 |
800 | 0 |
900 | 0 |
1000 | 0 |
这是200个活动时的记录:
100级别的活动似乎并不能让算法体现出性能差距,我决定以1000为起点,以500为递增值再次执行。
数据个数 | 活动安排时间1(ms) | 活动安排时间2(ms) | 活动安排时间3(ms) | 活动安排时间(ms) |
1000 | 0 | 0 | 0 | 0 |
1500 | 0 | 0 | 0 | 0 |
2000 | 0 | 0 | 0 | 0 |
2500 | 0 | 0 | 0 | 0 |
3000 | 985400 | 0 | 0 | 328466.6667 |
3500 | 0 | 1018700 | 0 | 339566.6667 |
4000 | 998600 | 996900 | 0 | 665166.6667 |
4500 | 0 | 995900 | 0 | 331966.6667 |
5000 | 1001000 | 1008000 | 0 | 669666.6667 |
5500 | 998100 | 964700 | 998000 | 986933.3333 |
6000 | 1002500 | 1001000 | 996500 | 1000000 |
6500 | 999000 | 965000 | 997100 | 987033.3333 |
7000 | 930900 | 997500 | 991700 | 973366.6667 |
7500 | 996100 | 999100 | 999800 | 998333.3333 |
8000 | 997700 | 997000 | 1000000 | 998233.3333 |
8500 | 997200 | 998200 | 1000100 | 998500 |
9000 | 1995400 | 998000 | 1997300 | 1663566.667 |
9500 | 1994000 | 1001400 | 1000100 | 1331833.333 |
10000 | 996800 | 997700 | 995300 | 996600 |
时间随活动个数的增长而变长,基本符合O(n)
这是10 000个随机开始和结束时间的活动的安排输出:
- 结论
6.1 最长公共子序列问题
最长公共子序列的时间复杂度是O(mn)+O(m+n)。这一解法应用了动态规划思想。
就实验数据来看。因为数据过少似乎并不能准确地验证这一结论。需要后续的代码改进来验证。
另外,算法本身还可以进行二次优化。如果实验的目的只是求最长公共子序列的长度而不需要输出最长公共子序列。可以用两行的数组空间代替目前的设计。将空间需求降低至O(min(m,n))
6.2 活动安排问题
由于c语言的计时函数最多只能精确到ms,而程序执行通常的时间要小于这个数量级。因此我决定采用python代替c语言来完成活动安排问题。
活动安排问题的时间复杂度取决于排序的时间复杂度和安排主函数本身的时间复杂度。实验中我采用了冒泡排序,这是一种比较慢的排序方法。但活动安排主函数的测试还是比较顺利的。活动安排主函数的时间复杂度是O(n)
- 程序源码
7.1 最长公共子序列问题(c)
#include <stdio.h>
#include <string.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>
#define MAXLEN 601
void LCSLength(int m, int n, char *x, char *y, int c[][MAXLEN], int b[][MAXLEN])
{
int i, j;
for (i = 0; i <= m; i++)
c[i][0] = 0;
for (j = 1; j <= n; j++)
c[0][j] = 0;
for (i = 1; i <= m; i++)
{
for (j = 1; j <= n; j++)
{
if (x[i - 1] == y[j - 1])
{
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j] = 1;
}
else if (c[i - 1][j] >= c[i][j - 1])
{
c[i][j] = c[i - 1][j];
b[i][j] = 3;
}
else
{
c[i][j] = c[i][j - 1];
b[i][j] = 2;
}
}
}
}
void LCS(int i, int j, char *x, int b[][MAXLEN])
{
if (i == 0 || j == 0)
return;
if (b[i][j] == 1)
{
LCS(i - 1, j - 1, x, b);
printf("%c ", x[i - 1]);
}
else if (b[i][j] == 3)
{
LCS(i - 1, j, x, b);
}
else
{
LCS(i, j - 1, x, b);
}
}
void genRandomString(char* buff, int length)
{
char metachar[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
int i = 0;
srand((unsigned) time(NULL)); //用时间做种,每次产生随机 数不一样
for (i = 0; i < length; i++)
{
buff[i] = metachar[rand() % 62];
}
buff[length] = '\0';
}
int main()
{
char x[600]={0};
genRandomString(x,600);
printf("%s",x);
printf("\n============================\n");
Sleep(1000);
char y[600]={0};
genRandomString(y,600);
printf("%s",y);
printf("\n============================\n");
//char x[MAXLEN] = {"ABCBDAB"};
//char y[MAXLEN] = {"BDCABA"};
int c[MAXLEN][MAXLEN];
int b[MAXLEN][MAXLEN];
int len_x, len_y;
len_x = strlen(x);
len_y = strlen(y);
//time test
clock_t stime1 = clock();
LCSLength(len_x, len_y, x, y, c, b);
clock_t etime1 = clock();
clock_t stime2=clock();
LCS(len_x, len_y, x, b);
clock_t etime2=clock();
printf("\nLCSLength time is %d ms\n",etime1-stime1);
printf("LCS time is %d ms",etime2-stime2);
return 0;
}
7.2 活动安排问题(python)
import time
import random
def Order(activities,n):
for i in range(0,n):
for j in reversed(range(i+1,n)):
if activities[j][1]<activities[j-1][1]:
activities[j],activities[j-1]=activities[j-1],activities[j]
def ActivitiesArrange(a,n):
b =[0]
end=a[0][1]
for i in range(1,n):
if a[i][0]>=end:
b.append(i)
end=a[i][1]
return b
if __name__ == '__main__':
n=10000
activities=[[0 for col in range(2)] for row in range(n)]
for k in range(n):
startt = random.randint(0,n)
activities[k][0]=startt
endtt=random.randint(0,n)
activities[k][1]=endtt
for p in activities:
if p[0]>=p[1]:
p[0],p[1]=p[1],p[0]
# for q in activities:
# print(q)
start_order=time.time_ns()
Order(activities,n)
end_order=time.time_ns()
start_aa=time.time_ns()
b=ActivitiesArrange(activities,n)
end_aa=time.time_ns()
print("Total arranged activities: "+str(len(b))+".\n")
for i in b:
print(activities[i])
print("\nOrder time is :%s ns"%(end_order-start_order))
print("\nActivity arrange time is :%s ns"%(end_aa-start_aa))
项目源代码:github地址