ch2 递归与分治策略
这部分考核内容:
二分搜索技术
代码
/**
* 二分搜索核心代码
*
* @param a: 查询数组
* @param x: 查询的值
* @param n: 数组a的个数
* @return 查找到的下标值,若没有找到返回-1
*/
int BinarySearch(Type a[], const Type& x, int n)
{
int left = 0;
int right = n-1;
while(left <= right)
{
int middle = (left+right)/2;
if(x == a[middle])
return middle;
else if(x > a[middle])
left = middle + 1;
else
right = middle - 1;
}
return -1;
}
合并排序
例子
序列为{8, 5, 2, 4, 9}
代码
/**
* 合并排序算法
*
* @param a: 查询数组
* @param left: 左指针下标
* @param right: 右指针下标
* @return
*/
void MergeSort(Type a[], int left, int right)
{
if(left < right)
{
int i = (left+right)/2; //取中点
MergeSort(a, left, i);
MergeSort(a, i+1, right);
Merge(a, b, left, i, right);//合并到数组b
Copy(a, b, left, right); //复制回数组a
}
}
/**
* 合并数组:将c[1:m]和c[m+1:r]合并到d[1:r]
*
* @param c 待合并数组
* @param d 合并完成的数组
* @param l 左指针下标
* @param m 中间指针下标
* @param r 右指针下标
* @return
*/
void Merge(Type c[], Type d[], int l, int m, int r)
{
int i = l;
int j = m+1;
int k = l;
while((i <= m) && (j <= r))
{
if(c[i] <= c[j])
d[k++] = c[i++];
else
d[k++] = c[j++];
}
if(i>m) //前半段全都取走了,后半段剩余直接搬走
{
for(int q=j; q<=r; q++)
d[k++] = c[q];
}
else
{
for(int q=i; q<=m; q++)
d[k++] = c[q];
}
}
代码陈述
取中间元素分为左右两段,分别对左右两段排序,合并两段有序的序列,对左右两段的排序是递归使用这个方法
大事化小,如何体现
开始对于一个大数组进行排序,将其切分成两个小数组,对这两个小数组进行排序
快速排序
例子
代码
/**
* 快排核心代码
*
* @param a: 待排序数组
* @param p: 左边界
* @param r: 右边界
* @return
*/
void QuickSort(Type a[], int p, int r)
{
if(p < r)
{
int q = Partition(a, p, r);
QuickSort(a, p, q-1); //对左半段排序
QuickSort(a, q+1, r); //对右半段排序
}
}
/**
* 找标杆点位置
*
* @param a: 目标数组
* @param p: 左边界
* @param r: 右边界
* @return 标杆下标
*/
int Partition(Type a[], int p, int r)
{
int i = p;
int j = r+1;
Type x = a[p];
// 将小于x的元素交换到左边区域,将大于x的元素交换到右边区域
while(true)
{
while(a[++i]<x && i<r) ;//找到大于等于标杆元素 退出
while(a[--j]>x) ; //找到小于标杆元素 退出
if(i >= j)
break; //i,j异常,则退出
Swap(a[i], a[j]); //交换
}
//把标杆换到位
a[p] = a[j];
a[j] = x;
return j;
}
代码陈述
对于输入的数组,首先选取一个标杆,然后用两个指针指向剩余的元素两段,左指针找到比标杆大的值,右指针找到比标杆小的值,然后交换,左右指针异常时,就确定了标杆的位置在右指针位置。然后对于该标杆左右两段继续使用上述方法
大事化小,如何体现
本来是对一个大数组进行排序,一轮下来变成对两个小数组进行排序即可
ch3 动态规划
这一部分考核内容:
矩阵连乘问题
例子
老师所给示例
书上示例
转移方程
代码
/**
* 矩阵连乘核心代码
*
* @param p: 矩阵列数(第一个为数为 矩阵行数)
* @param n: 矩阵个数
* @param m: m[i][j]表示第i个矩阵到第j个矩阵,这样的矩阵串最优方案时,所需的最少数乘次数
* @param s: 断点位置
* @return
*/
void MatrixChain(int *p, int n, int **m, int **s)
{
for(int i=1; i<=n; i++) //填充对角线
m[i][i] = 0;
for(int r=2; r<=n; r++) //r是段长
{
for(int i=1; i<=n-r+1; i++) //i是段起点
{
int j=i+r-1; //j是段终点
// 第一个矩阵一段,后面矩阵一段,计算数乘次数
m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];
s[i][j] = i;
for(int k=i+1; k<j; k++) //遍历所有断裂的情况
{
// 在k处断裂,计算此时数乘次数
int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if(t < m[i][j]) //更新
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
}
大事化小,如何体现
开始对于1到n个矩阵求最少数乘次数,裂开变成对两段矩阵求最少数乘次数加上两段的乘积次数
图像压缩
例子
转移方程
代码
/**
* 图像压缩核心代码
*
* @param n: 像素个数
* @param p: 每个像素的值
* @param s: s[i]表示前i个像素的最优存储位数
* @param l: l[i]表示第i段的最优段长
* @param b: 每个像素的长度
* @return
*/
void Compress(int n, int p[], int s[], int l[], int b[])
{
int Lmax = 256, header = 11;
s[0] = 0;
for(int i=1; i<=n; i++)
{
b[i] = length(p[i]);
int bmax = b[i];
s[i] = s[i-1] + bmax; //一个像素为一段
l[i] = 1;
for(int j=2; j<=i&&j<=Lmax; j++) //j是各种合法段长的长度
{
if(bmax < b[i-j+1])
bmax = b[i-j+1]; //最长的位数
if(s[i] > s[i-j] + j*bmax) //若更优,则更新
{
s[i] = s[i-j] + j*bmax;
l[i] = j;
}
}
s[i] += header;
}
}
大事化小,如何体现
开始是对n个像素进行压缩的最优方案,转变为前面n-k个像素压缩和最后k个像素单独压缩之和的最优方案
0-1背包问题
例子
转移方程
代码
/**
* o-1背包问题的动态规划算法
*
* @param v: 各物品的价值
* @param w: 各物品的重量
* @param c: 背包容量
* @param n: 物品数量
* @param m: m[i][j]表示容量为j,从物品i~n中选择时的最优选法
* @return
*/
void Knapsack(Type v, int w, int c, int n, Type **m)
{
int jMax = min(w[n]-1, c);
//填写最后一行
for(int j=0; j<=jMax; j++)
m[n][j] = 0;
for(int j=w[n]; j<=c; j++)
m[n][j] = v[n];
for(int i=n-1; i>1; i--) //从倒数第2行填到顺数第2行
{
jMax = min(w[i]-1, c);
for(int j=0; j<=jMax; j++)
m[i][j] = m[i+1][j]; //装不下物品i
for(int j=w[i]; j<=c; j++) //能装入 在装入和放弃中选优
m[i][j] = max(m[i+1][j], m[i+1][j-w[i]]+v[i]);
}
m[1][c] = m[2][c];
if(c >= w[1])
m[1][c] = max(m[1][c], m[2][c-w[1]]+v[1]); //算第一行
}
大事化小,如何体现
开始是从n个物品中选择最优的选择方案,变成从后面n-1个物品中选择的最优方案与第1个物品装入与否之间的最优方案比较,依次递归
ch4 贪心算法
这一部分似乎没有考核内容
单源最短路径
- Dijkstra算法的基本思想是,设置顶点集合S,并不断地做贪心选择来扩充这个集合
- 一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知
- 设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度
实现代码
//4.5 单源最短路径
#include<iostream>
using namespace std;
#define maxint 10000
template<class Type>
/**
* 单源最短路径
*
*@param n 结点个数
*@param v 源点编号
*@param dist[i] 源结点v到结点i的最短路径长度
*@param prev[i] 到达结点i最短路径时,上一个结点编号
*@param c[i][j] 存放有向图中i到j的边的权
*@return
*/
void Dijkstra(int n, int v, Type *dist, int *prev, Type (*c)[6])
{
bool s[maxint]; //用于记录红点集
/*
初始化
*/
for(int i=1; i<=n; i++)
{
dist[i] = c[v][i];
s[i] = false;
if(dist[i] == maxint)
prev[i] = 0;
else
prev[i] = v;
}
dist[v] = 0;
s[v] = true;
/*
core
*/
for(int i=1; i<n; i++)
{
int temp = maxint;
int u = v;
for(int j=1; j<=n; j++)
if((!s[j]) && (dist[j]<temp)) //选出不在红点集 且 路径最短的结点
{
u = j;
temp = dist[j];
}
s[u] = true;
for(int j=1; j<=n; j++) //更新到所有未加入红点集节点的最短距离
{
if((!s[j]) && (c[u][j] < maxint))
{
Type newdist = dist[u] + c[u][j];
if(newdist < dist[j])
{
dist[j] = newdist;
prev[j] = u;
}
}
}
}
}
int main()
{
int n=5;
int v=1;
int dist[6] = {0};
int prev[6] = {0};
int c[6][6];
c[1][2]=10; c[1][4]=30; c[1][5]=100;
c[2][3]=50;
c[3][5]=10;
c[4][3]=20; c[4][5]=60;
Dijkstra(n, v, dist, prev, c);
for(int i=2; i<=n; i++){
cout<<"结点1到结点"<<i<<"的最短路径:"<<dist[i]<<"\t";
cout<<"前一个结点是"<<prev[i]<<endl;
}
return 0;
}
ch5 回溯法
这部分考核内容:
n后问题
解空间
代码
/**
* 查看第k行皇后是否与上面冲突
*
* @param k 行数
* @return 是否合法
*/
bool Queen::Place(int k)
{
for(int j=1; j<k; j++)
if((abs(k-j) == abs(x[j]-x[k])) || (x[j] == x[k])) //对角线或同一列
return false;
return true;
}
/**
* n后问题核心算法
*
* @param t 当前处理的行数
* @return
*/
void Queen::Backtrack(int t)
{
if(t > n)
sum++;
else
{
for(int i=1; i<=n; i++)
{
x[t] = i;
if(Place(t))
Backtrack(t+1);
}
}
}
0-1背包问题
解空间
代码
/**
* 计算上界
*
* @param i 选择物品从i往后
* @return 返回价值上界
*/
template<class Typew, class Typep>
Typep Knap<Typew, Typep>::Bound(int i)
{
Typew cleft = c-cw; //背包剩余空间
Typep b = cp; //当前价值
while(i<=n && w[i]<=cleft) //以物品单位重量价值递减序装入物品
{
cleft -= w[i];
b += p[i];
i++;
}
if(i <= n) //分割物品,装满背包
b += p[i]*cleft/w[i];
return b;
}
图的m着色(染色)问题
解空间
代码
/**
* 染色是否冲突
*
* @param k 顶点k
* @return 是否
*/
bool Color::Ok(int k)
{
for(int j=1; j<=n; j++)
if((a[k][j]==1) && (x[j]==x[k])) //检查相邻和颜色是否相同
return false;
return true;
}
/**
* 染色问题核心代码
*
* @param t 第t个顶点
* @return
*/
void Color::Backtrack(int t)
{
if(t>n) //完成所有点的染色
{
sum++; //合法的解个数
for(int i=1; i<=n; i++)
cout<< x[i] << ' ';
cout<<endl;
}
else //未完成所有点的染色
{
for(int i=1; i<=m; i++)
{
x[t] = i; //给t号结点染第i种颜色
if(Ok(t)) //判断是否合法
Backtrack(t+1); //无冲突,则继续处理第t+1个结点
x[t] = 0; //洗白,恢复现场
}
}
}
旅行售货员问题
解空间
x数组的变化情况
略微模糊
为了更加清晰易看,下面图是上图的部分切分版本
x数组的变化情况
j=2, j=3
及其子节点回溯情况
j=2, j=4
及其子节点回溯情况
ch6 分支限界法
这部分的考核内容:
分支限界法要素
- 解空间
- 界(解空间中说事,解空间的点)
- 当叶子结点出现在队列头部时,该结点为最优
单源最短路径问题
界是什么量
- 在代码中是使用
length
(当前路长,或使用cc
当前开销) - 解空间树中的结点所对应的当前路长是以该结点为根的子树中所有结点对应路长的一个下界
对于给定数据,队列的变化情况
陈述算法
首先将源节点入队列,该队列是按照当前路长最短进行排列的优先队列,每次都从队列头部取出一个节点,将其扩展的子节点入队,然后再从队列头部取出一个节点,重复上述过程,直到终点节点第一次出现在队头为止,此时该终点的路径为最优
0-1背包问题
界是什么量
- 在代码中使用的是
uprofit
(结点的价值上界) - 解空间树中以结点N为根的子树树任一结点的价值都不超过N的
uprofit
,即 根结点的uprofit
比子孙的uprofit
都大
对于给定数据,队列的变化情况
上图圈圈中有三个数,最顶上那个数是当前背包重量,中间的那个数是当前背包价值,最底下那个数是当前背包的上界
陈述算法
首先各物品按其单位重量价值从大到小排序
- 按
uprofit
由大到小进入最大堆 - 解空间根结点入队
- 取队头,计算头结点的各个孩子,孩子入队
- 对头若为叶结点,算法结束,否则重复上一步骤
旅行售货员问题
界是什么量
- 在代码中使用的是
lcost
(最低消费) - 解空间树中的结点对应的
lcost
是以该结点为根的子树中所有结点对应的lcost
的一个下界
对于给定数据,队列的变化情况
说明:
- 上图中的
minout
为每个顶点的最小费用出边 - 各点的
lcost
计算是当前开销+离开其余未到达的点的最小值 - 例如:
c点lcost
=当前开销(30元)+离开2的钱最少(5元)+离开3的钱最少(5元)+离开4的钱最少(4)= 44
陈述算法
首先先统计出每个顶点的最小费用出边minout
- 按
lcost
由小到大进入优先队列 - 解空间根结点入队
- 取队头,计算头结点的各个孩子的
lcost
,孩子入队 - 对头若为叶结点,算法结束,否则重复上一步骤
电路板排列问题
界是什么量
- 在代码中使用的是
cd
(当前密度) - 解空间树中的结点对应的
cd
是以该结点为根的子树中所有结点对应的cd
的一个下界
对于给定数据,队列的变化情况
说明:解空间树中第一层的含义是1号槽放第1/2/3号电路板,其余层同理
陈述算法
- 按
cd
由小到大进入优先队列 - 解空间根结点入队
- 取队头,计算头结点的各个孩子的
cd
,孩子入队 - 结束情况有两种
- 若当前扩展结点的
cd
bestd
,则优先队列中其余结点都不可能导致最优解,算法结束 - 若已排定n-1块电路板,则算法结束
- 若当前扩展结点的
说明:bestd
表示目前遇到的每块板子插好时的最优密度
ch7 随机化算法
这部分的考核内容:
n后问题(拉斯维加斯算法)
代码
/**
* 随机放置n个皇后的拉斯维加斯算法
*
* @return 是否有解
*/
bool Queen::QueensLV(void)
{
RandomNumber rnd; //随机数产生器
int k = 1; //下一个放置的皇后编号
int count = 1;
while((k<=n) && (count>0)) //上行有解,且未到最后一行
{
count = 0;
for(int i=1; i<=n; i++) //统计并记录当前本行的所有合法位置
{
x[k] = i; //k行i列
if(Place(k)) //判断是否位置是否合法
y[count++] = i;
}
if(count>0)
x[k++] = y[rnd.Random(count)]; //从合法位置中随机选一个
}
return (count>0); //count>0表示所有皇后放置成功
}
素数测试(蒙特卡罗算法)
相关知识
费尔马小定理:如果p是一个素数,且0<a<p,则ap-1≡1(mod p)
- 利用费尔马小定理,对于给定的整数n,可以设计素数判定算法
- 通过计算d=2n-1mod n 来判定整数n的素性
- 当d≠1时,n肯定不是素数
- 当d=1时,n 很可能是素数
- 费尔马小定理毕竟只是素数判定的一个必要条件,满足费尔马小定理条件的整数n未必全是素数。
- 有些合数也满足费尔马小定理的条件,这些合数被称为Carmichael数,前3个Carmichael数是561、1105、1729。Carmichael数是非常少的,在1~100 000 000的整数中,只有255个Carmichael数
二次探测定理:如果p是一个素数,且0<x<p,则方程x2≡1(mod p)的解为x=1,p-1
- 事实上,x2≡1(mod p)等价于x2-1≡0(mod p)。由此可知(x-1)(x+1)≡0(mod p),故p必须整除x-1或x+1,由于p是素数且0<x<p,推出x=1或x=p-1
- 利用二次探测定理,可以在利用费尔马小定理计算an-1mod n 的过程中增加对整数n的二次探测。一旦发现违背二次探测条件,即可得出n不是素数的结论
- 下面是算法power用于计算apmod n,并在计算过程中实施对n的二次探测
/** * 费尔马小定律并实施n的二次探测 * a^p mod n =result * * @param a: 费尔马小定律中的底数 * @param p: 需要判断是否为素数的数字 * @param n: * @param result: 计算结果 * @param composite: * @return */ void power(unsigned int a, unsigned int p, unsigned int n, unsigned int &result, bool &composite) { unsigned int x; if(p==0) result = 1; else { power(a, p/2, n, x, composite); // 递归计算 result = (x*x)%n; //二次探测 (A*B)%n = (A%n)*(B%n) if((result==1) && (x!=1) && (x!=n-1)) composite = true; if((p%2)==1) result = (result*a) % n; } }
算法陈述
- 对于数n,在1、2、…、n-1中随机抽样a
- 若a不让an-1≡1(mod n)成立,则n是合数
- 若a使an-1≡1(mod n)成立,则重复抽样k次,若都成立,则倾向于n是素数,误判的概率为(1/4)k
误判概率
- 当n充分大时,1~n-1的基数中有1/4个数是使费尔马小定律成立的,但n未必是素数,所以我们随机选到这样一个基数的概率为1/4
- 若进行k次测试,误判的概率为(1/4)k