zzuli算法复习大三上
分值占比
一、(20 5 * 4)四道解递归方程
二、(40 5 * 2+10 * 3 )分析阅读代码和伪代码回答时间复杂度等问题,动态规划最长公共子序列,动态规划、分治的原理步骤以及改进策略。
三、(40 20+20)大题: 回溯/分支限界20分、 最大连续子序列求/货物装载(20分)
主定理法
我们要处理一个 规模为 的问题通过分治,得到
个规模为
的问题,分解子问题和合并子问题的时间是 :

三种判断:

分治法
-
分治法的设计思想:对于一个规模为n的问题,若该问题可以容易地解决(例如n的规模较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题相互独立且与原问题形式相同,递归地解决子问题,然后将各子问题的解合并得到原问题的解,这种算法的策略叫做分治法。
-
分治法的步骤
- 分解成若干个问题:将原问题分解成若干个规模较小,相互独立,与原问题相同的子问题
- 求解子问题:若子问题规模较小,容易被解决,则直接求解,否则递归地求解各个子问题
- 合并子问题:将各个子问题合并。
-
分治法的改进策略
- 代数变换,减少子问题个数
- 增加预处理,利用预处理减少递归内部的计算量
-
分治法的适用情况:1 该问题的规模缩小到一定的程度就可以容易地解决。2 该问题可以分解为若干个规模较小的相同问题。3 利用该问题分解出的子问题的解可以合并为该问题的解。4该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
-
循环赛日程表

/* 将n=2k问题划分为4部分: (1)左上角:左上角为2k-1个选手在前半程的比赛日程(k=1时直接给出,否则,上一轮求出的就是2k-1个选手的比赛日程)。 (2)左下角:左下角为另2k-1个选手在前半程的比赛日程,由左上角加2k-1得到,例如22个选手比赛,左下角由左上角直接加2(2k-1)得到,23个选手比赛,左下角由左上角直接加4(2k-1)得到。 (3)右上角:将左下角直接复制到右上角得到另2k-1个选手在后半程的比赛日程。 (4)右下角:将左上角直接复制到右下角得到2k-1个选手在后半程的比赛日程。 */ #include <stdio.h> #define MAX 101 //问题表示 int k; //求解结果表示 int a[MAX][MAX]; //存放比赛日程表(行列下标为0的元素不用) void Plan(int k) { int i,j,n,t,temp; n=2; //n从2^1=2开始 a[1][1]=1; a[1][2]=2; //求解2个选手比赛日程,得到左上角元素 a[2][1]=2; a[2][2]=1; for (t=1;t<k;t++) //迭代处理2^2(t=1)…,2^k(t=k-1)个选手 { temp=n; //temp=2^t n=n*2; //n=2^(t+1) for (i=temp+1;i<=n;i++ ) //填左下角元素 for (j=1; j<=temp; j++) a[i][j]=a[i-temp][j]+temp; //产生左下角元素 for (i=1; i<=temp; i++) //填右上角元素 for (j=temp+1; j<=n; j++) a[i][j]=a[i+temp][(j+temp)% n]; for (i=temp+1; i<=n; i++) //填右下角元素 for (j=temp+1; j<=n; j++) a[i][j]=a[i-temp][j-temp]; } } -
二分和归并排序,注意时间复杂度求解。
§例2.1 设 是一个给定实数,计算 ,其中n为自然数。
时间复杂度,暴力法o(n) 分治法,o(logn)

最长连续子序列
-
暴力枚举法
-
三维略
-
二维优化
int maxSubSum2(int a[],int n) { int i,j; int maxSum=a[0],thisSum; for (i=0;i<n;i++) { thisSum=0; for (j=i;j<n;j++) { thisSum+=a[j]; //maxSum已经包含了a[i..j-1]的最大和 if (thisSum>maxSum) maxSum=thisSum; } } return maxSum; } -
一维优化
优化思路:如果扫描中遇到负数,当前子序列和thisSum将会减小,若thisSum为负数,表明前面已经扫描的那个子序列可以抛弃了,则放弃这个子序列,重新开始下一个子序列的分析,并置thisSum为0。
int maxSubSum3(int a[],int n) { int i,maxSum=0,thisSum=0; for (i=0;i<n;i++) { thisSum+=a[i]; if (thisSum<0) //若当前子序列和为负数,重新开始下一子序列 thisSum=0; if (maxSum<thisSum) //比较求最大连续子序列和 maxSum=thisSum; } return maxSum; }若这个子序列和thisSum不断增加,那么最大子序列和maxSum也不断增加。
-
-
分治策略
long maxSubSum(int a[],int left,int right) //求a[left..high]序列中最大连续子序列和 { int i,j; long maxLeftSum,maxRightSum; long maxLeftBorderSum,leftBorderSum; long maxRightBorderSum,rightBorderSum; if (left==right) //子序列只有一个元素时 { if (a[left]>0) //该元素大于0时返回它 return a[left]; else //该元素小于或等于0时返回0 return 0; } int mid=(left+right)/2; //求中间位置 maxLeftSum=maxSubSum(a,left,mid); //递归求左边 maxRightSum=maxSubSum(a,mid+1,right); //递归求右边 maxLeftBorderSum=0,leftBorderSum=0;// 初始化最大Left子序列和,Left子序列和 for (i=mid;i>=left;i--) //求出以左边加上a[mid]元素 { leftBorderSum+=a[i]; //构成的序列的最大和 if (leftBorderSum>maxLeftBorderSum) //更新Left最大值 maxLeftBorderSum=leftBorderSum; } maxRightBorderSum=0,rightBorderSum=0; for (j=mid+1;j<=right;j++) //求出a[mid]右边元素 { rightBorderSum+=a[j]; //构成的序列的最大和 if (rightBorderSum>maxRightBorderSum) maxRightBorderSum=rightBorderSum; } return max3(maxLeftSum,maxRightSum, //return三者最大值 maxLeftBorderSum+maxRightBorderSum); } -
动态规划策略
-
表示方法:对于含有n个整数的序列a,设b,表示以a[i]结尾的最大子序列和,则b;表示以a[li-1]为结尾的最大连续子序列和。
-
核心转移方程

-
核心代码
//问题表示,省略 int n=6; int a[]={0,-2,11,-4,13,-5,-2}; //a数组不用下标为0的元素 //求解结果表示 int dp[MAXN]; void maxSubSum() //求dp数组 { dp[0]=0; for (int j=1;j<=n;j++) dp[j]=max(dp[j-1]+a[j],a[j]); } -
数组变化

-
-
时间复杂度比较

最长公共子序列
void LCSlength() //求dp
{ int i,j;
for (i=0;i<=m;i++) //将dp[i][0]置为0,边界条件
dp[i][0]=0;
for (j=0;j<=n;j++) //将dp[0][j]置为0,边界条件
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++) //两重for循环处理a、b的所有字符
{ if (a[i]==b[j]) //情况(1)
dp[i][j]=dp[i-1][j-1]+1;
else //情况(2)
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
}
void Buildsubs() //由dp构造最长公共子序列
{ int k=dp[m][n]; //k为a和b的最长公共子序列长度
int i=m;
int j=n;
while (k>0) //在subs中放入最长公共子序列(反向)
if (dp[i][j]==dp[i-1][j])
i--;
else if (dp[i][j]==dp[i][j-1])
j--;
else //与上方、左边元素值均不相等
{ printf(a[i]); //最长公共子序列中有a[i]
i--; j--; k--;
}
}
01背包问题

//问题表示
int n=5,W=10; //5种物品,限制重量不超过10
int w[MAXN]={0,2,2,6,5,4}; //下标0不用
int v[MAXN]={0,6,3,5,4,6}; //下标0不用
//求解结果表示
int dp[MAXN][MAXW];
int x[MAXN];
int maxv; //存放最优解的总价值
void Knap() //动态规划法求0/1背包问题
{ int i,r;
for (i=0;i<=n;i++) //置边界条件dp[i][0]=0
dp[i][0]=0;
for (r=0;r<=W;r++) //置边界条件dp[0][r]=0
dp[0][r]=0;
for (i=1;i<=n;i++)
{ for (r=1;r<=W;r++)
if (r<w[i]) //放不下该物品,则最大价值等于不放该物品的价值
dp[i][r]=dp[i-1][r];
else
dp[i][r]=max(dp[i-1][r],dp[i-1][r-w[i]]+v[i]);
}
}
void Buildx() //回推求最优解
{ int i=n,r=W;
maxv=0;
while (i>=0) //判断每个物品
{
if (dp[i][r]!=dp[i-1][r])
{ x[i]=1; //选取物品i
r=r-w[i];
}
else
x[i]=0; //不选取物品i
i--;
}
}
资源分配问题

//问题表示
int m=3,n=5; //商店数为m,总人数为n
int v[MAXM][MAXN]={{0,0,0,0,0,0},{0,3,7,9,12,13},
{0,5,10,11,11,11},{0,4,6,11,12,12}}; //不计v[0]行
//求解结果表示
int dp[MAXM][MAXN];
int pnum[MAXM][MAXN];
void Plan() //求最优方案dp
{ int maxf,maxj;
for (int j=0;j<=n;j++) //置边界条件
dp[m+1][j]=0;
for (int i=m;i>=1;i--) //i从商店3到1进行处理
{ for (int s=1;s<=n;s++) //分配的总人数为s
{ maxf=0;
maxj=0;
for (j=0;j<=s;j++) //找该商店最优情况maxf和分配人数maxj
{ if ((v[i][j]+dp[i+1][s-j])>=maxf)
{ maxf=v[i][j]+dp[i+1][s-j];
maxj=j;
}
}
dp[i][s]=maxf;
pnum[i][s]=maxj;
}
}
}
dp[m+1][j]=0 //边界条件(类似终点的dp值为0)
dp[i][s]=max(v[i][j]+dp[i+1][s-j]) //pnum[i][s]=dp[i][s]取最大值的j(0≤j≤n)
多重背包问题

/*
设置二维数组fk,其中fk[i][j]存放dp[i][j]得到最大值时物品i挑选的件数。
dp[i][j]=MAX{dp[i-1][j-k*w[i]]+k*v[i]} 当dp[i][j] <
dp[i-1][j-k*w[i]]+k*v[i](k*w[i]≤j)
fk[i][j]=k; 物品i取k件
*/
//问题表示
int n,W;
int w[MAXN],v[MAXN];
//求解结果表示
int dp[MAXN+1][MAXW+1],fk[MAXN+1][MAXW+1];
int solve() //求解多重背包问题
{ int i,j,k;
for (i=0;i<=n;i++)
for (j=0;j<=W;j++) {dp[i][j]=0; fk[i][j]=0};
for (i=1;i<=n;i++)
{ for (j=0;j<=W;j++)
for (k=0;k*w[i]<=j;k++)
{ if (dp[i][j]<dp[i-1][j-k*w[i]]+k*v[i])
{ dp[i][j]=dp[i-1][j-k*w[i]]+k*v[i];
fk[i][j]=k; //物品i取k件
}
}
}
return dp[n][W];
}
回溯法
-
解空间为子集树
int x[n]; //x存放解向量,全局变量 void backtrack(int i) //求解子集树的递归框架 { if(i>n) //搜索到叶子结点,输出一个可行解 输出结果; else //当前节点可以扩展,存在子节点 { for (j=下界;j<=上界;j++) //用j枚举i所有可能的路径 { x[i]=j; //产生一个可能的解分量 if (constraint(i) && bound(i)) backtrack(i+1); //满足约束条件和限界函数,继续下一层 } } } -
解空间为排列树
int x[n]; //x存放解向量,并初始化 void backtrack(int i) //求解排列树的递归框架 { if(i>n) //搜索到叶子结点,输出一个可行解 输出结果; else { for (j=i;j<=n;j++) //用j枚举i所有可能的路径,后面还没有排列的元素 { 为第i层节点选择x[j],并保证排列中每个元素不同; if (constraint(i) && bound(i)) backtrack(i+1); //满足约束条件和限界函数,进入下一层 } } }7行:排列树和子集树算法框架最大的差别在于:j的取值意义和X[i]的取值意义不同!
-
求解货物装载问题
【问题描述】有n个集装箱要装上一艘载重量为w的轮船,其中集装箱i(1≤i<n)的重量为wi。不考虑集装箱的体积限制,现要这些集装箱中选出若干装上轮船,使它们的重量之和小于等于W,当总重量相同时要求选取的集装箱个数尽可能少。
如果换成让集装箱的个数尽可能多呢?如果再换成让集装箱的体积尽可能大呢?
例如,n=5,W=10,wi={5,2,6,4,3}时,其最佳装载方案是(0,0,1,1,0),即装载第3、4个集装箱。//void dfs(int num,int tw,int rw,int op[],int i) //其中num表示选择的集装箱个数,tw表示选择的集装箱重量和,rw表示剩余集装箱的重量和,op表示一个 //解,即一个选择方案,i表示考虑的集装箱i。 void dfs(int num,int tw,int rw,int op[],int i) //考虑第i个集装箱 { if (i>n) //找到一个叶子结点 { if (tw==W && num<minnum) { maxw=tw; //找到一个满足条件的更优解,保存它 minnum=num; for (int j=1;j<=n;j++) x[j]=op[j]; //复制最优解 } } else //尚未找完所有集装箱 { op[i]=1; //选取第i个集装箱 if (tw+w[i]<=W) //左孩子结点剪枝:装载满足条件的集装箱 dfs(num+1,tw+w[i],rw-w[i],op,i+1); op[i]=0; //不选取第i个集装箱,回溯 if (tw+rw>W) //右孩子结点剪枝 dfs(num,tw,rw-w[i],op,i+1); } }问题二:【子集和问题描述】给定n个不同的正整数集合w= (w,w,,)和一个正数w,要求找出w的子集s,使该子集中所有元素的和为
w。
例如,当n=4时,w= (11,13,24,7),W=31,则满足要求的子集为(11,13,7)和(24,7) 。解空间树

-
01背包问题
(1)选择第i个物品放入背包:op[i]=1,tw=tw+w[i],tv=tv+v[i],转向下一个状态Knap_dfs(i+1,tw,tv,op)。该决策对应左分枝。
(2)不选择第i个物品放入背包:op[i]=0,tw不变,tv不变,转向下一个状态Knap_dfs(i+1,tw,tv,op)。该决策对应右分枝。//问题表示 int n=4; //4种物品 int W=6; //限制重量为6 int w[]={0,5,3,2,1}; //存放4个物品重量,不用下标0元素 int v[]={0,4,4,3,1}; //存放4个物品价值,不用下标0元素 //求解结果表示 int x[MAXN]; //存放最终解 int maxv; //存放最优解的总价值 void Knap_dfs(int i,int tw,int tv,int op[]) //求解0/1背包问题 { if (i>n) //找到一个叶子结点 { if (tw<=W && tv>maxv) //找到一个满足条件的更优解,保存 { maxv=tv; for (int j=1;j<=n;j++) x[j]=op[j]; } } else //尚未找完所有物品 { op[i]=1; //选取第i个物品 Knap_dfs(i+1,tw+w[i],tv+v[i],op); op[i]=0; //不选取第i个物品,回溯 Knap_dfs(i+1,tw,tv,op); } }
123
//左剪枝
else //尚未找完所有物品
{ if (tw+w[i]<W) //左孩子结点剪枝
{ op[i]=1; //选取第i个物品
Knap_dfs (i+1,tw+w[i],tv+v[i],op);
}
op[i]=0; //不选取第i个物品,回溯
Knap_dfs (i+1,tw,tv,op);
}
//右剪枝
else //尚未找完所有物品
{ if (tw+w[i]<=W) //左孩子结点剪枝
{ op[i]=1; //选取第i个物品
Knap_dfs (i+1,tw+w[i],tv+v[i],rw-w[i],op);
}
op[i]=0; //不选取第i个物品,回溯
if (tw+rw>W) //右孩子结点剪枝,也就是总重量超过才继续递归扩展。
Knap_dfs (i+1,tw,tv,rw-w[i],op);
}

分枝限界法
分枝限界法的步骤:
-
第一步:确定限界函数:
限界函数把后面所有节点都扩展能够得到可能的最小成本。
也就是说只有限界函数<当前最小解才值得扩展! -
第二步:通过分枝限界法画出解空间树:
在约束条件不成立时减枝;
在限界函数>当前最小解时剪枝。 -
第三步:套用回溯法算法框架,并增加约束条件。



浙公网安备 33010602011771号