算法复杂度是算法运行所需要的计算机资源的量,需要时间资源的量称为时间复杂度,需要空间资源的量称为空间复杂度
大O符号:
f(n)=O(g(n))读作f(n)是g(n)的大O,g(n)是f(n)的渐进上界。
例:7n-2是O(n),读作7n-2是n的大O,n是7n-2的一个上界。
log(n²)是O(log(n)),log(n)是log(n²)的一个上界
常数c < logn < n^1/2 < n < nlogn < n² < 2^n < n!
1.递归的两个必要因素: 边界条件 / 递归方程
2.分治法的设计思想,将一个难以直接解决的大问题,分割成一些规模较小的相同问题/子问题,以便各个击破,分而治之。
3.分治法所能解决的问题一般具有以下几个特征:
1)该问题的规模缩小到一定程度就可以容易地解决;
2)该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质
3)利用该问题分解出的子问题的解,可以合并为该问题的解
4)该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共子问题
4.分治法的步骤:分解、求解子问题、合并子问题
5.分治法的时间复杂度
if n == 1:
T(n) = O(1)
else if n > 1:
T(n) = k*T(n/m) + f(n)
# k为将原问题划分k个子问题,每个子问题的规模为n/m,f(n)为合并子问题的时间
降低时间复杂度思路:减小k,减少子问题个数
大整数乘法
大整数乘法改进分治算法的思路:减少子问题的个数
function MULT(X,Y,n) {
S=SIGN(X)*SIGN(Y); //S为X和Y的符号乘积
X=ABS(X);
Y=ABS(Y); //X和Y分别取绝对值
if n==1 then
return S*X*Y;
else {
A=X的左边n/2位;
B=X的右边n/2位;
C=Y的左边n/2位;
D=Y的右边n/2位;
ml=MULT(A,C,n/2);
m2=MULT(A-B,D-C,n/2);
m3=MULT(B,D,n/2);
S=S*(m1*pow(10,n)+(m1+m2+m3)*pow(10,n/2)+m3);
return(S);
}
}
快速排序(写第一次排序后顺序)
棋盘覆盖
title = 1;//骨牌编号
board[100][100];//棋盘
function ChessBoard(tr, tc, dr, dc, size) {
if(size == 1) return;
s = size/2;
title += 1;
//处理左上角棋盘
if(dr < tr + s && dc < tc + s ) Chess(tr,tc,dr,dc,s);
else{
board[tr+s-1][tc+s-1] = title;
Chess(tr,tc,tr+s-1,tc+s-1,s);
}
//处理右上角棋盘
if(dr < tr + s && dc >= tc + s) Chess(tr, tc + s, dr, dc, s);
else{
board[tr+s-1][tc+s] = title;
Chess(tr,tc+s,tr+s-1,tc+s,s);
}
//处理左下角棋盘
if(dr >= tr + s && dc < tc + s ) Chess(tr+s,tc,dr,dc,s);
else{
board[tr+s][tc+s-1] = title;
Chess(tr+s,tc,tr+s,tc+s-1,s);
}
//处理右下角棋盘
if(dr >= tr + s && dc >= tc + s ) Chess(tr+s,tc+s,dr,dc,s);
else{
board[tr+s][tc+s] = title;
Chess(tr+s,tc+s,tr+s,tc+s,s);
}
}
1.动态规划需要满足以下条件
(1)最优子结构性质
(2)子问题重叠性质
2.动态规划的步骤
(1)分析最优解的性质和结构特性
(2)递归地定义最优值
(3)以自底向上(递推)的方式求解最优值
(4)根据最优值的信息构造最优解
矩阵连乘
//n为连乘矩阵的个数 b存储了n个矩阵的阶数下标
//m[i][j]计算Mi×Mi+1×...×Mj的最小值
//t[i][j]是Mi×Mi+1×...×Mj的最佳断开位置
void MultiplyMatrix(b[], n, m[][], t[][]) {
//初始化m[][]
for i=1 to n
for j=1 to n
m[i][j]=MAX;
//初始化对角线
for i=1 to n m[i][i] = 0;
//相乘矩阵的个数
for r=2 to n
for i=1 to n-r+1 {
int j=i+r-1;
for(int k=i;k<j;k++){//遍历切割位置,找到最小值
int temp=m[i][k]+m[k+1][j]+b[i-1]*b[k]*b[j];
//每次用更小的temp更新m[i][j]
if(temp<m[i][j]){
m[i][j]=temp;
t[i][j]=k;
}
}
}
}
0-1背包
V[n][C];//最优值表格
//w是重量数组,v是价值数组,n是物品个数,C是背包容量
function KnapSack(w[], v[], n, C) {
//第0列初始化为0
for i = 0 to n
V[i][0] = 0
//第0行初始化为0
for j = 0 to C
V[0][j] = 0
//递归填表
for i = 1 to n
for j = 1 to C
//小于该物品重量的列等于上一行
if(j < w[i]) then V[i][j]=V[i-1][j];
else then V[i][j] = max(V[i-1][j],V[i-1][j-w[i]]+v[i]);
j = c, i = n;
while (i>0)
if(V[i][j]>V[i-1][j])
then
x[i] = 1;
j = j - w[i];
else then x[i] = 0;
i--;
return x and V[n][C] //返回最优解数组和背包的最大价值
}
最长公共子序列
//1)计算lcs长度
function LCSLength(X,Y) {
m= length(X);
n=length (Y);
//初始化第一行和第一列
for i=1 to m
C[i][0]=0;
for j=1 to n
C[0][j]=0;
for i=1 to m
for j=1 to n
if (x[i]==y[j]) {
B[i][j] = ‘\’;
C[i][j] = C[i-1][j-1] + 1;
} else if (C[i-1][j]>=C[i][j-1]) {
B[i][j] = ‘|’;
C[i][j] = C[i-1][j];
} else {
B[i][j] = ‘-‘;
C[i][j] = C[i][j-1];
}
return C and B
}
//2)构造一个lcs
PRINT_LCS(B, X, i, j) {
If (i==0 or j==0) return;
if (B[i][j]==‘\’) {
PRINT_LCS(B, X, i-1, j-1);
Print(X[i]);
} else if (B[i][j]==‘|’) {
PRINT_LCS(B, X, i-1, j);
} else {PRINT_LCS(B, X, i, j-1);}
}
贪心算法在每一步做出局部最优的贪心选择,这种局部最优选择并不总能获得最优解,但通常能够获得近似最优解
特点:
部分背包问题可以用贪心算法求解(根据物品的单位价值量排序进行选择),而0-1背包我们不能用贪心算法求解。
动态规划主要运用于二维或三维问题,而贪心一般是一维问题,他们都具有最优子结构性质,都用于求解最优化问题。
算法 | 基本思想 | 依赖子问题的解 | 解问题的方向 | 最优解 | 复杂程度 |
---|---|---|---|---|---|
贪心算法 | 贪心选择 | 否 | 自顶向下 | 局部最优 | 简单有效 |
动态规划 | 递归定义填表 | 是 | 自底向上 | 整体最优 | 较复杂 |
活动安排
struct meet {
int s;//开始时间
int f;//结束时间
}
//会议i的起始时间si和结束时间fi
int[] GreedySelector(int n,meet m[],int A[])
//m[] 按m[i].f的非减序排列
{
A[1] = 1; //开始只包含1
j = 1; i = 2; //从i(2)开始找与j不相容的会议
while(i<=n)
{
if(m[i].s>=m[j].f)
{
A[i] = 1;
j=i;
}
else A[i]=0;
i++;
}
return A;//返回可行解数组
}
哈夫曼编码(左小右大,画出树和各个叶子结点的编码)
最小生成树(两种方法prim和kruscal,一般只考最后结果)
最短路(填表)
基本思想:先定义问题的解空间,然后在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。
在遍历过程中,为减少无效搜索,应用约束条件、目标函数等剪枝函数进行剪枝。
两种剪枝函数:约束函数和限界函数
同时使用约束条件和目标函数的界进行裁剪的是0/1背包问题
只使用约束条件进行裁剪的是N皇后问题
回溯法的算法框架按照问题的解空间一般分为子集树算法框架和排列树算法框架。
用回溯法解0-1背包问题时,该问题的解空间结构为子集树结构,旅行商问题的解空间树是排列树,N皇后问题的解空间树是排列树,图的m着色问题的解空间树是子集树。
回溯法的效率依赖于满足显式约束的值的个数、计算约束函数的时间、计算限界函数的时间,不依赖于确定解空间的时间。
n皇后
class Queen {
n; //皇后个数
sum = 0; //可行解个数
x[N]; //皇后放置的列数
//判断是否能放置皇后的函数,返回1可以放置,返回0不可
place(row) {
for (i = 1; i < row; i++) {
//判断是否在同一列或同一对角线
if (abs(row-i)==abs(x[row]-x[i])||x[row]==x[i]) return 0;
}
return 1;
}
//递归回溯函数
queen(t) {
if(t>n && n>0)
sum++
else {
for (i = 1; i <= n; i++) {
//第t个皇后放在第i列
x[t] = i;
//如果可以放置,递归放置下一个皇后
if(place(t)) queen(t+1);
}
}
//返回可行解个数
return sum;
}
}
旅行商
int g[N][N];//图的邻接矩阵
//a[]存当前解,st[]是标记数组,ans为最短路径距离 n是顶点个数
int a[N], st[N], n, ans = MAX;
int path[N];//最优解路径数组
//递归回溯函数
void dfs(int x){
if(x == n + 1){
a[n+1] = a[1];
int nowans = 0;
for i=1 to n {
if(g[a[i]][a[i+1]] == MAX) return; // 两点之间没有边
nowans += g[a[i]][a[i+1]];
}
if(ans > nowans){
for i=1 to n+1
path[i] = a[i];
ans = nowans;
}
return;
}
for i=1 to n {
if(st[i]) continue;
a[x] = i; // 第x个数选i
st[i] = 1;
dfs(x + 1);
st[i] = 0; // 回溯
}
}
基本思想:按广度优先搜索遍历问题的解空间树,在遍历过程中,对已经处理的每一个节点根据限界函数估算目标函数的可能取值。适用于求解最优化问题。
0/1背包(画树)
旅行商(画树)
分支限界法与回溯法比较
算法 | 解空间树搜索方式 | 存储结点的常用数据结构 | 结点存储特性 | 常用应用 |
---|---|---|---|---|
回溯法 | 深度优先搜索 | 栈 | 活结点的所有可行子节结点 | 找出满足约束条件的所有解 |
分支限界法 | 广度优先或最小耗费优先搜索 | 队列、优先队列 | 每个结点只有一次成为活结点的机会 | 找出满足约束条件的一个解或特定意义下的最优解 |