算法是由若干条指令组成的有穷序列,且满足下述4条性质:
① 输入:有零个或多个由外部提供的量作为算法的输入;
② 输出:算法产生至少一个量作为输出;
③ 确定性:组成算法的每条指令是清晰的,无歧义的;
④ 有限性:算法中每条指令的执行次数是有限的,执行每条指令的时间也是有限的。
程序是算法用某种程序设计语言的具体实现。可以不满足性质④。
— 可以在多项式内求解的【判定问题】。 P类问题是确定性计算模型下的易解问题类。
— 所有的非确定性多项式时间可解的【判定问题】构成NP类问题 。
— 非确定性计算模型下的易验证问题类。
非确定性算法:非确定性算法将问题分解成猜测和验证两个阶段。算法的猜测阶段是非确定性的,算法的验证阶段是确定性的,它验证猜测阶段给出解的正确性。
NP中的某些问题的复杂性与整个类的复杂性相关联,这些问题中任何一个如果存在多项式时间的算法,那么所有NP问题都是多项式时间可解的.
======================================================================================
P ≠ NP ,至今无明确的解答。
1、合取范式的可满足性问题 (CNF-SAT)
布尔可满足性问题 ( Boolean Satisfiability Problem , SAT ) , 是 NPC 的 ;
2、三元合取范式的可满足性问题 ( 3-SAT 或 3-CNF-SAT)
3、团问题CLIQUE
4、顶点覆盖问题 (VERTEX-COVER)
给定一个 无向图 G,G 的 点集覆盖 定义 ;找到 无向图 G 的 点集子集 V ,使得无向图 G 中的任何一条边 , 都与 点集子集 V 的至少一个节点是接触的 ;
5、子集和问题 (SUBSET-SUM)
给定一个自然数集合, 给定一个自然数 t, 问给定的自然数集合中,是否存在子集, 使它们之和等于给定的自然数 t
6、哈密顿回路问题 (HAM-CYCLE)
**给定一个图G=(V,E), 是否存在一条路线,从一个起点出发,找到一条回路,每个顶点且只走过一次**
7、旅行售货员问题 (TSP)
无向图中 , 每条边都有一个权重 , 求是否有一条哈密顿路径的权重之和 , 不超过给定的自然数 W
插入排序图解
转载-插入排序(图解)
收录-归并排序详解
【对于给定的函数 g(n) 】
递归函数 :用函数自身给出定义的函数
递归中的【递】即【入栈】,递进;【归】–>【出栈】,回归。
递归算法:一个算法包含对自身的调用 (这种调用可以是直接的,也可以是间接的)
【优点】结构清晰,可读性强,易用数学归纳法证明
【缺点】运行效率较低
【相关代码】
// 斐波那契数列的实现
// 1、 递归实现 时间复杂度 —– O(2^N)
int fibo1(int n)
{
if(n == 0 || n == 1) return n;
return fibo1(n - 1) + fibo1(n - 2);
}
// 2、 非递归实现
// 时间复杂度为O(N),空间复杂度为O(N)
int* fibo2(int n)
{
int* pArr = new int[n + 1];
pArr[0] = 0; pArr[1] = 1;
for(int i = 2; i <= n; i++)
pArr[i] = pArr[i - 1] + pArr[i - 2];
return pArr;
}
// 时间复杂度为O(N),空间复杂度为O(1)
int fibo3(int n)
{
int nFirst = 0, nSecond = 1, nThird = 0;
for(int i = 2 ; i <= n; i++) {
nThird = nFirst + nSecond;
nFirst = nSecond;
nSecond = nThird;
}
return nThird;
}
将正整数n表示成一系列正整数之和: n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1
正整数n的这种表示称为正整数n的划分
正整数n的不同的划分个数称为正整数n的划分数p(n)
目标:求正整数n的不同划分个数p(n)
【 引入m,最大加数 ≤ m 的划分个数, 记作q(n,m) 】
1、n==1:m取任意值,只有一种划分{1}
2、m==1:n取任意值,只有一种划分{1+1+1+…+1}
3、n==m:根据划分中是否包含n,分为:
(1)划分中包含n,只有一种{n}
(2)划分中不包含n,这时划分中最大的数字也一定比n小,即n所有的n-1划分
4、n 5、n>m:根据划分中是否包含最大值m,可以分为
(1)包含m 的情况,即{m, {x1,x2,….xi} 且 x1+x2+…+xi=n-m,因此这种情况下为 q(n-m,m)。
(2)划分中不包含m且划分中所有值都比m小,即n的所有(m-1)划分,为 q(n,m-1)
【问题】 有 A,B,C 三根柱子,A 上面有 n 个盘子,我们想把 A 上面的盘子移动到 C 上,但是要满足以下三个条件:① 每次只能移动一个盘子; ② 盘子只能从柱子顶端滑出移到下一根柱子; ③ 盘子只能叠在比它大的盘子上。
【分析】n = 1 时,直接把盘子从 A 移到 C ;
n > 1 时,
先把上面 n - 1 个盘子从 A 移到 B(子问题,递归);
再将最大的盘子从 A 移到 C ;
再将 B 上 n - 1 个盘子从 B 移到 C(子问题,递归)。
代换法(substitution method) 递归树方法(recursion-tree method) 主方法(master method)
猜测解的形式 用数学归纳法证明之
【参考】https://blog.csdn.net/yangtzhou/article/details/105339108
3 表示我们将一个问题分解为3 个子问题;
n / 4 则表明每个子问题的规模是原问题的1 / 4;
T( ) 表明的为递归形式;
c n^2 表明为合并需要的时间,其中c为常数系数c > 0 。其实也就是算法度Θ ( n^2 )
T(n) = a T(n/b) + f(n)
a>=1, b>1, a和b均为常数 ,f(n)是渐近正函数
分解(Divide):将原问题分解为子问题
解决(Conquer):求解子问题
合并(Combine):组合子问题的解得到原问题的 解
【设计思想】将一个难以解决的大问题分割成一些规模较小的相同子问题。
问题一般具有以下特征
问题的规模缩小到一定程度就可以容易地解决
问题可以分解为若干个规模较小的相同问题,即该 问题具有最优子结构性质
基于子问题的解可以合并为原问题的解
问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题
—— 使子问题规模尽量接近的做法
在使用分治法和递归时,要尽量把问题分成规模相等,或至少是规模相近的子问题以提高算法的效率
直接求解:Θ(n)
QuickSort(A,p,r)
if p A[j]
exchange A[i+1]<->A[r] //最终确定主元的位置
return i+1 //返回主元的位置
end
快速排序的随机化版本: 随机选择一个元素作为主元
F(n) = F(n-1) + F(n-2)
递归算法:
void max_min(int a[],int l,int r,int &max,int &min){
int n=r-l+1;
int p=n/2;
if(n==1) { //只有一个元素
max=a[l];
min=a[l];
}
else if(n==2){ //只有两个元素
max=a[l]>a[r] ? a[l]:a[r];
min=a[l]rmax ? lmax:rmax;
min=lmin
【参考】
leetcode-612
https://blog.csdn.net/qq_40452317/article/details/88403397
https://blog.csdn.net/qq_41891805/article/details/105709605
在一个由元素组成的集合里,第i个**顺序统计量(order statistic)**是该集合第i小的元素。
https://www.cnblogs.com/zhoutaotao/p/4047082.html
一般情况 O(N), 最坏情况 O(N^2)
RandomSelect(A,p, q,k) //随机选择统计,以期望线性时间做选择
{
if (p==q) return A[p];
int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进行划分为两部分
int i=pivot-p+1;
if (i==k )return A[pivot];
else if (i
RANDOMIZED-SELECT(A, p, r, i)
1 if p = r
2 then return A[p]
3 q ← RANDOMIZED-PARTITION(A, p, r)
4 k ← q - p + 1
5 if i = k // the pivot value is the answer
6 then return A[q]
7 elseif i < k
8 then return RANDOMIZED-SELECT(A, p, q - 1, i)
9 else return RANDOMIZED-SELECT(A, q + 1, r, i - k)
https://www.cnblogs.com/bakari/p/4852452.html
(1)将输入数组的n个元素划分为n/5(上取整)组,每组5个元素,且至多只有一个组有剩下的n%5个元素组成
(2)寻找每个组中的中位数。首先对每组中的元素(至多为5个)进行插入排序,然后从排序后的序列中选择出中位数。
(3)对第2步中找出的n/5(上取整)个中位数,递归调用SELECT以找出其中位数x。(如果是偶数去下中位数)
(4)调用PARTITION过程,按照中位数x对输入数组进行划分。确定中位数x的位置k。
(5)如果i=k,则返回x。否则,如果i
【分析】所有元素分为 n/5 组,在 x 左右分别有 (n/5) / 2 = n / 10 组,
当 x > k 时,至少有 (n/10) * 3 个数 < x ;当 x < k 时,至少有 (n/10) * 3 个数 > x ;
因此最多在剩余的 7n / 10 个元素中查找,时间为 T (7n/10) ;
综上, T(n) = T(n/5) + T(7n/10) + O(n)
。。。
。。。
工程问题中设计参数的选择 有限资源的合理分配 车间作业调度 交通系统的规划 ……
分治法求解回顾
子问题相互独立,不包含公共子问题
动态规划
与分治法类似,也是将问题分解为规模逐渐减小的同类型的子问题
与分治法不同,分解所得的子问题很多都是【重复】的
若一个问题可以分解为若干个高度重复的子问题,且问题也具有最优子结构性质, 就可以用动态规划法求解
具体方式:可以递推的方式逐层计算最优值并记录必要的信息,最后根据记录的信息构造最优解
保存已解决的子问题的答案,在需要时使用,从而避免大量重复计算
找出最优解的性质,并刻画其结构特征
递归地定义最优值(写出动态规划方程)
以【自底向上】的递推方式计算出最优值
根据计算最优值时得到的信息,以递归方法构造一个最优解
https://www.cnblogs.com/Monster-su/p/14574240.html
子序列:将给定序列中零个或多个元素去掉之后得到的结果
子串:给定串中任意个【连续】的字符组成的子序列称为该串的子串
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
//i,j从1开始,所以下面用i-1和j-1使得可以从数组0元素开始
if(x[i-1]==y[j-1]) c[i][j] = c[i-1][j-1]+1;
else if(c[i-1][j]>=c[i][j-1]) c[i][j]=c[i-1][j];
else c[i][j]=c[i][j-1];
}
}
https://www.cnblogs.com/Monster-su/p/14579852.html
// 动态规划
public static void f1(char arr1[],char arr2[],int n,int m) {
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
if(arr1[i-1]==arr2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=Math.max(dp[i-1][j], dp[i][j-1]);
}
}
System.out.println(dp[n][m]);
}
// 备忘录
public static int f2(char arr1[],char arr2[],int n,int m) {
if(n==0||m==0) return 0;
else if(arr1[n-1]==arr2[m-1])
dp[n][m]=f2(arr1,arr2,n-1,m-1)+1;
else
dp[n][m]=Math.max(f2(arr1,arr2,n-1,m),f2(arr1,arr2,n,m-1));
return dp[n][m];
}
https://www.cnblogs.com/wenjieyatou/p/6898876.html
题目:有三种硬币,面值2, 5, 7,买一本书需要27元,如何用最少的硬币整好付清。
根据检索频率(包括节点p和虚节点q)设计一颗二叉搜索树,使得期望搜索代价最小。
根节点 a[k] 的最有二分搜索树,将左、右子树分别接在根节点下:
【改进 P48】 递推求cij及记录Tij的根的算法可以改进, 把算法时间复杂度从Θ(n3)降到Θ(n2)
参考:https://www.cnblogs.com/wkfvawl/p/11667092.html
最优调度 :① 使M1上的加工是无间断的。即M1上的加工时间是所有ai之和,但M2上不一定是bi之和。
② 使作业在两台机器上的加工次序是完全相同的。
数组中的元素只是在需要计算时才去计算,计算采用递归方式,值计算出来之后将其保存起来以备它用。
备忘录方法是递归方式是【自顶向下】的, 而动态规划是【自底向上】的递归的。
因此备忘录方法的控制结构与直接递归方法的控制结构相同, 区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
计算C[i,j]的递归算法LCS_L2(X,Y, i,j,C)
1、 若x[i] == y[j],则去检查C[i-1,j-1]
若C[i-1,j-1]> -1(已经计算出来),就直接把C[i-1,j-1]+1赋 给C[i,j],返回
若C[i-1,j-1]=-1(尚未计算出来),就递归调用LCS_L2(X,Y, i-1,j-1,C) 计算出C[i-1,j-1],然后再把
C[i-1,j-1]+1赋给C[i,j] , 返回
2、若x[i] != y[j],则检查C[i-1,j]和C[i,j-1]
若两者均 > -1(已经计算出来),则把max{ C[i-1,j], C[i,j-1]} 赋给C[i,j],返回
若C[i-1,j], C[i,j-1] 两者中有一个等于-1(尚未计算出来), 或两者均等于-1,就递归调用LCS_L2将其计
算出来,然后 再把max{ C[i-1,j], C[i,j-1]} 赋给C[i,j]
【解一:O(n^2)的dp】
// Dynamic programming.
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0) return 0;
int[] dp = new int[nums.length];
int res = 0;
Arrays.fill(dp, 1);
for(int i = 0; i < nums.length; i++) {
for(int j = 0; j < i; j++) {
if( nums[j] < nums[i] )
dp[i] = Math.max(dp[i], dp[j] + 1);
}
res = Math.max(res, dp[i]);
}
return res;
}
【解二:结合LCS】将原序列A排序得到序列B,求A和B的LCS
【解三:贪心 + 二分查找】
https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-2/
class Solution {
public int lengthOfLIS(int[] nums) {
int len = 1, n = nums.length;
if (n == 0) return 0;
int[] dp = new int[n + 1];
dp[len] = nums[0];
for (int i = 1; i < n; ++i) {
if (nums[i] > d[len]) dp[++len] = nums[i];
else {
int l = 1, r = len, pos = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (dp[mid] < nums[i]) {
pos = mid; // 更新位置
l = mid + 1;
} else {
r = mid - 1;
}
}
// 【注意】所更新的数组并不是LIS,只是记录了长度
dp[pos + 1] = nums[i];
}
}
return len;
}
}
Wavio是一个整数序列,具有如下特性
1、简单算法
利用3个for循环,记录所有字段的和,比较所得最大字段和,时间复杂度O(n^3)
2、简单算法改进
从技巧改进,去除最后一层for循环,减少重复计算,时间复杂度O(n^2)
**3、分治策略 ** T(n)=O(nlogn)
。。。
// 初始化
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
// =================================================================================== //
// 【滚动数组】 注意:****** 遍历背包容量的顺序
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 ★★★
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
n种物品,背包容量M,物品i重量为weight[i],数量有num[i]个,价值为value[i]
在基本的0-1背包问题、完全背包和多重 背包的基础上,将三者混合起来。
对于每件物品,有两种不同的费用,即装一件物品,需要消耗两种代价
给出种物品和一个容量为的背包,每种物品都有无限件。
这n个物品被划分为若干组,每组中的物品相互冲突,最多选一件放入背包
0-1背包加入依赖关系,如果物品依赖于物品,则表 示如果要选物品,则必须先选物品。
贪心算法并不从整体最优上加以考虑,它所作出的选择只是在某种意义上的**【局部最优选择】**
1)【问题描述】
有一个需要使用每个资源的n个活动组成的集合S= {a1,a2,···,an },资源每次只能由一个活动使用。每个活动ai都有一个开始时间si和结束时间fi,且 0≤si
2)解决步骤
—— 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到 。
自底向上
的方式解各子问题 ,每步所做的选择往往依赖于相关子问题的解。自顶向下
的方式进行 ,仅在当前状态下做出最好选择,即局部最优选择,然后再去解做出 此选择后产生的子问题。—— 一个问题的最优解包含其子问题的最优解
相同点:贪心算法和动态规划算法都要求问题具有最优子结构性质
区别:0-1背包问题,背包问题。
【问题描述】:给定 带权有向图 G =
基本思想 o ( n^2 )
设置顶点集合S,初始时,S中仅含有源,此后不断作 贪心选择 来扩充这个集合
一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知
设u是G的某一个顶点,把从源到u且中间只经过S中顶 点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度
Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改, 检查 dist(u)+[u,j] 与 dist[j] 的大小,若 dist(u)+[u,j] 较小,则更新
一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点之间的最短路径长度
【问题描述】:G =(V,E)是无向连通带权图,即一个网络, E中每条边(v,w)的权为c[v][w]
, 若G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树,生成树上各边权的总和为该生成树的耗费,耗费最小的生成树称为G的最小生成树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IVcQ7mqQ-1648911071051)(C:\Users\Weller.Y\AppData\Roaming\Typora\typora-user-images\1638599310066.png)]
void Prim(int n, Type **c) {
T = ; S = 1;
while(S != V) {
(i, j) = i∈S且j∈V-S的最小权边;
T = T ∪ {(i ,j)} ;
S = S ∪ {j} ;
}
}
// 把原图中所有边按权值排序
// 初始化MST为空,以及初始化连通分量
for(int i = 0; i < m; i++)
if(e[i].u 和 e[i].v 不在同一连通分量) {
把边 e[i] 加入 MST
合并 e[i].u 和 e[i].v 所在的连通分量
}
一种可变字长编码(VLC)方式; 广泛应用于数据文件的压缩; 【前缀码】;
【构建方法】: 每次都选择根节点“频率”值最小的两棵树合并
给定字符集C={c1,c2,...,cn},每个字符ci都有相应的频率fi
根据字符集构建结点集S={s1,s2,...,sn},每个结点si保存有字符ci和频率fi的信息
while |S| != 1 do
取出S中频率最小的两个结点x和y;
构造父节点z;
z.f = x.f + y.f;
z.c = undefined;
z.left = x;
z.right = y;
将x和y从S中移走,将z加入S;
endWhile
此时S[0]就是根节点,返回根节点
。。。
在少数应用中,可能出现求不出解的情况
但一旦找到一个解,这个解一定是正确的
在求不出解时,需再次调用算法进行计算, 直到获得解为止
对于此类算法,主要是分析算法的时间复杂度的期望值,以及调用一次产生失败 (求不出解)的概率
当一个确定性算法在最坏情况和平均情况下差别较大时可在这个确定性算法中引入随机性将之改造成一个舍伍德算法;引入随机性不是为了消除最坏,而是为了减少最坏和特定实例的关联性。
【指纹】令I(x)是x的编码,取Ip(x) ≡ I(x) (mod p)作 为x的指纹,p是一个小于M的素数
首先由A发一个x的长度给B,若长度不等,则x≠y
若长度相等,则采用“取指纹”的方法:
步骤:
本算法可以转成Las Vegas算法: 当Ip(Y)=Ip(X(j))时,不直接return j,而去 比较Y和X(j)
【问题描述】设给定n个元素,从n个数中随机地选取m个数(m≤n),m个数指互不相同的
上述算法存在的问题 ①
【主元素】设T[1:n]是一个含有n个元素的数组。当 {i|T[i]=x}|>n/2时,称元素x是数组T的主元素
// 判断是否 含有主元素
public static boolean majority(int[]t, int n) {
// 判定主元素的蒙特卡罗算法 rnd = new Random();
int i=rnd.random(n)+1; int x=t[i];
// 随机选择数组元素
int k=0;
for (int j=1;j<=n;j++)
if (t[j]==x) k++;
return (k>n/2); // k>n/2 时t含有主元素
}
多次重复调用,计算时间与调用次数有关
(1) 费马小定理: 满足Fermat条件的数未必全是素数
当 为素数,则有 (0
若 a^(p-1) != 1 (mod p),则 p 必为合数 (2)二次探测:如果 是一个素数,, 则方程 的解为 或 利用二次探测定理,可以在基于Fermat条件判断时,增加二次探测,若违背二次探测条件,则可得出不是素数的结论。 棋盘上相继的各行中随机地放置皇后,并注意使新放置的皇后与已放置的皇后互不攻击,直至n个皇后均已相容地放置好,或已没有下一个皇后的可 放置位置时为止 n后问题的Las Vegas算法思路: 各行随机放置皇后,使新放的与已有的互不攻击, until ( n皇后放好 || 无可供下一皇后放置的位置) 回溯法的基本做法是搜索,它是一种可以**【避免不必要搜索】的 【穷举式搜索法】** 回溯法适用于求解一些**【组合数较大】**的问题 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树. 问题的解空间一般用解空间树的方式组织 深度优先的问题状态生成法。 如果对一个扩展结点R,一旦产生了它的一个儿子C, 就把C当做新的扩展结点。在完成对子树C(以C为 根的子树)的穷尽搜索之后,将R重新变成扩展结 点,继续生成R的下一个儿子(如果存在) 回溯法:为了避免生成那些不可能产生最优解的问题状态,要不断地利用限界函数 (bounding function)来“处死”那些实际上 不可能产生所需解的活结点,从而减少问题的计算量 具有**【限界函数的深度优先生成法】**称为回溯 法 (1) 针对所给问题,定义问题的解空间 (2) 确定易于搜索的解空间结构 (3) 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索 用约束函数在扩展结点处剪去不满足约束 的子树 用限界函数剪去得不到最优解 的子树 在搜索至树上任意一点时,先判断该节点 对应部分解是否满足约束条件,或是否超 出目标函数的界 判断该节点是否包含问题的(最优)解。 • 不包含,则跳过对以该节点为根的子树的搜索, 剪枝(pruning) • 包含,则进入以该节点为根的子树,继续按 深度优先搜索 递归回溯 — 回溯法对解空间作深度优先搜索,因此, 在一般情况下用递归方法实现回溯法 迭代回溯 — 用树的非递归深度优先遍历算法,可将回溯法表示为一个非递归迭代过程 子集树 - O(2^n):当所给问题是从n个元素的集合S 中找出S满足某种性质的子集时,相应的解空间树 排列树 - O(n!) :当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为 排列树 1)【问题描述】: 有一批共有 n 个集装箱要装上两艘载重量分别为 c1 和 c2 的轮船,其中集装箱 i 的重量为 w[i], 且重量之和小于(c1 + c2)。装载问题要求确定是否存在一个合理的装载方案可将这 n 个集装箱装上这两艘轮船。如果有,找出一种装载方案。 2)【问题分析】 容易去证明:如果一个装载问题有解,则采用下面的策略可以得到最优装载方案: ①首先将第一艘轮船尽可能装满;② 然后将剩余的集装箱装在第二艘轮船上。 3)【算法实现】 1)问题描述 每一个作业 Ji 都有两项任务分别在2台机器上完成。每个作业必须先由机器1处理,然后再由机器2处理。作业 Ji 需要机器 j 的处理时间为 tji。对于一个确定的作业调度,设 Fji 是作业 i 在机器 j 上完成处理时间。则所有作业在机器2上完成处理时间和 f=F2i ,称为该作业调度的完成时间和 【参考网址】 https://www.cnblogs.com/wkfvawl/p/11765576.html [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BYznKKyn-1648911071052)(C:\Users\Weller.Y\AppData\Roaming\Typora\typora-user-images\1638880968987.png)] 用n元组x[1:n]表示符号三角形的第一行的n个符号,当x[i]等于1时,表示符号三角形的第一行的第i个符号为“+”;当x[i]等于0时,表示符号三角形的第一行的第i个符号为“-”;1<=i<=n。由于x[i]是2值的。所以在用回溯法解符号三角形问题时,可以用一棵完全二叉树来表示其解空间。在符号三角形的第一行的前i个符号x[1:i]确定后,就确定了一个有i*(i+1)/2个符号组成的符号三角形。(i*(i+1)/2来自首项为1、公差为1的等差数列的求和公式) 下一步确定x[i+1]的值后,只要在前面已确定的符号三角形的右边加一条边,就可以拓展为x[1:i+1]所对应的符号三角形。最终由x[1:n]所确定的符号三角形包含的“+”个数与“-”同为n(n+1)/4(n(n+1)/2的一半,也就是一半的符号)。因此,在回溯可将“+”、“-”个数均 不超过n(n+1)/4为约束条件。同时,对于给定的n当n(n+1)/2为奇数时,显然不存在“+”和“-”个数相同的符号三角形。 计算时间为 O(n2^n)。 可行性约束函数:当前符号三角形所包含 的“+”个数与“-”个数均不超过n***(n+1)/4 https://www.cnblogs.com/wkfvawl/p/11766662.html 1)问题描述 在n*n格的棋盘上防止彼此不受攻击的n个皇后,按照国际象棋的规则,皇后可以攻击与之处在同一行、同一列 或同一斜线上的棋子。n后问题等价于,在n *n 的棋盘上防止n个皇后,任何两个不能满足攻击条件。 2)问题分析 https://cloud.tencent.com/developer/article/1424758 【核心代码】 解空间:子集树 1)问题描述 给定 n 种物品和一背包。物品 i 的重量是 wi > 0,其价值为 vi > 0,背包的容量为 c 。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大? (要求使用回溯法) 三着色问题:机场停机位分配是指根据航班和机型等属性,为每个航班指定停机位。要满足下列约束: 1) 各航班须被分配,且仅能被分配一个停机位; 2) 同一时刻同一停机位不可分配1个以上的航班; 3) 应满足航站衔接以及过站时间衔接要求; 4) 机位与使用机位航班应相互匹配。 1) 问题描述 某售货员要到若干城市去推销商品,已知各城市之间的路程,他要选定一条从驻地出发,经过每个城市一遍,最后回到住地的路线,使总的路程最短。 2) 问题分析 解空间树 —— 排列树 这个问题的解空间应该是一棵排列树。因为圆就是按照一定的顺序排在矩形框中的,这里面我们将圆的半径进行排序,从而代表圆排序。其中a=[r1,r2,…,rn]就是我们的序列。 相同点:是在问题的【解空间树】上搜索问题解的算法 不同点: https://blog.csdn.net/u010089444/article/details/74331907 https://blog.csdn.net/weixin_44712386/article/details/105532881 分支 —— 使用广度优先策略,依次生成扩展结点的所有分支。 分枝限界法常以广度优先或以最小耗费 (最大效益)优先的方式搜索问题的解空间树 按照队列先进先出(FIFO)原则选取下一个节点为 扩展节点 有一批共n个集装箱要装上2艘载重量分别 为c1和c2的轮船,其中集装箱i的重量为wi, 且∑ Wi ≤ c1 + c2 ; 装载问题要求确定是否有一个合理的装载 方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案 【算法解释】https://blog.csdn.net/qq_44766883/article/details/106904355 【详细步骤解释】https://my.oschina.net/wangshengzhuang/blog/785027 给定一个n顶点网络(有向或无向),找出一个包含n个顶点且具有最小耗费 的换路。任何一个包含网络所有顶点的换路称为一个旅行。**旅行商问题(Traveling Salesman Problem,TSP)**是要寻找一条耗费最少的旅行。 2)算法描述 https://blog.csdn.net/qq_44766883/article/details/106992785 P 问题是【容易解决】的问题,P类问题是多项式时间可解的 NP 问题是【容易验证】的问题,NP类问题是多项式时间可验证的 NPC问题(NP Complete):NP完全问题,所有NP问题在多项式时间内都能规约(Reducibility)到它的NP问题,即解决了此NPC问题,所有NP问题也都能得到解决。 NP-hard(NP hard):NP难问题,所有NP问题在多项式时间内都能规约(Reducibility)到它的问题,但不一定是NP问题。 归约(reduction): 归约是证明NP-hard问题的一种常用方法,通常用<=这个符号来表示。如P<=Q,这个就表示P is reducible to Q , or Q is the reduction from P or P is reduced to Q(P问题可以归约到Q问题,or可以把P归约到Q) 。这里的reduction的符号可以当成是 比较难易程度的小于等于号,意味着P至少比Q容易,或者Q至少比P难。 1.把P的输入转化到Q的输入; 2. 把Q的输出转化到P的输出。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CEjtKOnM-1648911071053)(C:\Users\Weller.Y\AppData\Roaming\Typora\typora-user-images\1639227159358.png)] 设IA是判定问题A的任一实例,B是另一判定 问题,若存在一个从A到B的映射f满足 则称问题A可多项式变换为B,记作A≤pB, (≤p亦称Karp规约) 若A≤pB且问题B是多项式时间可判定的, 则问题A也一定是多项式时间可判定的。 要证明一个判定问题B是NP-C的: ① 证明B是多项式时间可验证的(从而B属于 NP类) ② 还要找一个NP-C问题A,证明A 可以在多项式时间里变换为B,且A的任 一实例回答为‘Yes’ iff与之对应的B的实 例回答为‘Yes’ 许多具有实际意义的问题都是NP完全问题。我们不知道如何在多项式时间内求得最优解。但是,这些问题通常十分重要,我们不能因此而放弃对它们的求解。即使一个问题是NP完全的,也有其解次方法。 解决NP完全问题至少有三种方法: 1)如果实际输入数据规模较小,则用指数级运行时间的算法就能很好地解决问题; 2)对于一些能在多项式时间内解决的特殊情况,可以把它们单独列出来求解; 3)可以寻找一些能够在多项式时间内得到近似最优解(near-optimal solution)的方法(最坏情况或平均情况)。在实际应用中,近似最优解一般都能满足要求。返回近似最优解的算疫就称为近似算法(approximation algorithm)。 使用近似算法的目的: 近似算法其实是针对NP难问题的一种退让,对于许多P不等于NP的最优化问题,无法在多项式时间内找到最优解。因此,如果可以只求一个我们可以接受的解,而不是非要最优解,那么可能存在一个多项式时间的算法。 设有n个物体u1,u2,…,un,每个物体的体积不超过1。另外,有足够多的、体积为1的箱子。箱子、物体均是长方体且截面相同 ,问如何装箱,使得所用箱子数最少。 先将所有物品从大到小排序,然后再使用FF法 旅行商问题(TSP)简单描述 TSP的特殊性质 void approxTSP (Graph g) { (1)选择g的任一顶点r; (2)用Prim算法找出带权图g的一棵以r为根的最小生成树T; (3)前序遍历树T得到的顶点表L; (4)将r加到表L的末尾,按表L中顶点次序组成回路H,作为计算结果返回; } 法: 1)如果实际输入数据规模较小,则用指数级运行时间的算法就能很好地解决问题; 2)对于一些能在多项式时间内解决的特殊情况,可以把它们单独列出来求解; 3)可以寻找一些能够在多项式时间内得到近似最优解(near-optimal solution)的方法(最坏情况或平均情况)。在实际应用中,近似最优解一般都能满足要求。返回近似最优解的算疫就称为近似算法(approximation algorithm)。 使用近似算法的目的: 近似算法其实是针对NP难问题的一种退让,对于许多P不等于NP的最优化问题,无法在多项式时间内找到最优解。因此,如果可以只求一个我们可以接受的解,而不是非要最优解,那么可能存在一个多项式时间的算法。 设有n个物体u1,u2,…,un,每个物体的体积不超过1。另外,有足够多的、体积为1的箱子。箱子、物体均是长方体且截面相同 ,问如何装箱,使得所用箱子数最少。 先将所有物品从大到小排序,然后再使用FF法 旅行商问题(TSP)简单描述 TSP的特殊性质 void approxTSP (Graph g) { (1)选择g的任一顶点r; (2)用Prim算法找出带权图g的一棵以r为根的最小生成树T; (3)前序遍历树T得到的顶点表L; (4)将r加到表L的末尾,按表L中顶点次序组成回路H,作为计算结果返回; }n后问题
Las Vegas算法
七、回溯法 & 分支限界法
(一) 回溯法
1、图的基本知识
邻接表
邻接矩阵
广度优先搜索(BFS)
深度优先搜索(DFS)
2、回溯法知识
1) 回溯法的基本思想
2) 问题的解空间
生成问题状态的说明
生成问题状态的基本方法——DFS
生成问题状态的基本方法——回溯法
3)回溯法的基本思想
【常用剪枝函数】
【回溯法实现方式】
【子集树和排列树】
3、装载问题
// https://www.cnblogs.com/xymqx/p/3724356.html
void Loading
4、批处理作业调度
https://blog.csdn.net/m0_46308522/article/details/109425775int n; //作业数
int M[100][100]; //M[i][j]表示第i个作业在机器j上需要处理的时间
int x[100]; //x[i]表示第i个处理的作业为x[i]
int bestx[100]; //x[i]的最优值
int f1; //作业在机器1上完成处理的时间
int f2[100]; //f2[i]表示第i个作业在机器2上完成处理的时间
int f; //用于记录 前i个作业在机器2上完成处理的时间和
int bestf; //f的最优值
void Backtrack(int i)
{
if(i>n) //每到达一个叶子结点,一定是产生了一个最优解,因此要更新之前最优解的值
{
if(f
5、符号三角形问题
无解的判断**:n*(n+1)/2为奇数 public static int n, half, count;// 第一行的符号个数n,当前“+”个数count,
public static int[][] p;// 符号三角形矩阵
public static long sum;// 符合条件的符号三角形个数
public static void backtrack(int t) {
// 剪枝, 判断"+"或"-"是否都超过了一半
if ((count > half) || (t * (t - 1) / 2 - count > half))
return;// 若符号统计未超过半数,并且另一种符号也未超过半数
if (t > n) { //可行性约束
sum++;
} //当t>n时,算法搜索至叶节点,得新的“+”与“—”个数相同的符号三角形,当前已找到的符号三角形数sum增1.
else {
for (int i = 0; i <= 1; i++) {
p[1][t] = i;
count += i;
for (int j = 2; j <= t; j++) {
//这里可能有一些难推导,第t列的这个符号只能影响下面几行的t列之前的符号
//如果当前行为k,下面的行数为j,则t最多可以影响到的列数为t-(j-k)即为t-j+k
//在一行中即为t-j+1
/*
if (p[j - 1][t - j + 1] == p[j - 1][t - j + 2]) {
p[j][t - j + 1] = 1;
// 2个同号下面都是“+”
}
else {
p[j][t - j + 1] = 0;
// 2个异号下面都是“-”
}
*/
//用0,1表示的一个好处是可以使用异或运算,来计算下层结果
p[j][t-j+1] = p[j-1][t-j+1] ^ p[j-1][t-j+2];
count += p[j][t - j + 1];
}
backtrack(t + 1); // 回溯
for (int j = 2; j <= t; j++) {
// 回溯时取消上一次的赋值
count -= p[j][t - j + 1];
}
count -= i;
}
}
}
6、n后问题
// n 为输入的棋盘大小
// row 是当前递归到棋牌的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
if (row == n) {
result.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
// 检查皇后位置是否相容
bool isValid(int row, int col, vector<string>& chessboard, int n) {
int count = 0;
// 检查列
for (int i = 0; i < row; i++) // 这是一个剪枝
if (chessboard[i][col] == 'Q') return false;
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--)
if (chessboard[i][j] == 'Q') return false;
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++)
if (chessboard[i][j] == 'Q') return false;
return true;
}
7、0-1背包问题
//用于记录是否存放当前地物体
int inOut[n];
//保存最多的价值
int value_max;
//定义背包的总共的重量的
int bagVolume = c;
/*
描述:背包问题的约束条件,当传入对应的序号,就去判定是否要放对应的物品
参数:放入包中物体的序号
返回:当前物体总重量和背包容量的关系
true:表示没有超重
false:表示超重
原理:判定当前的物品的总重量,是不是小于物体的实际重量
*/
bool bagConstraint(int m, int weight[]) {
//一直遍历m层之前的所有物体,求出其对应的重量
int allweight = 0;
for (int i = 0; i <= m; ++i)
//计算出总共的重量的
allweight += inOut[i] * weight[i];
//比较当前的物体总重量和背包的总重量关系
return allweight <= bagVolume;
}
/*
描述:深度优先搜索的函数,递归函数
参数:m:是要装入背包的物品的数量
weight:是背包中各个物品的重量
value:是背包中各个物品的价值
返回:最终返回的是最大的价值
问题:
*/
void back_tracing_0-1bag(int m, int n, int weight[], int valueAll[]) {
//首先确定终止条件,那就比较最大值
if (m == n) {
int sum = 0;
for (int i = 0; i < m; ++i)
sum += valueAll[i] * inOut[i];
//比较最大值
if (sum > value_max) value = sum;
}
else {
//没有到达终止条件,继续向下进行递归
for (int i = 0; i < 2; ++i) {
inOut[m] = i;
//判定是否满足对应约束条件
if (bagConstraint(m, weight))
//满足约束条件,继续向下进行递归的
back_tracing_0-1bag(m + 1, n, weight, valueAll);
}
}
}
8、图的m着色问题
1.将数组color[n]初始化为0;
2.k=1;
3.while (k>=1)
3.1 依次考察每一种颜色,若顶点k的着色与其他顶点的着色不发生冲突,则转步骤3.2;否则,搜索下一个颜色;
3.2 若顶点已全部着色,则输出数组color[n],返回;
3.3 否则, 3.3.1 若顶点k是一个合法着色,则k=k+1,转步骤3 处理下一个顶点;
3.3.2 否则,重置顶点k的着色情况,k=k-1,转步 骤3回溯;
图的m着色问题应用举例
9、旅行售货员问题
10、圆排列问题
// https://blog.csdn.net/weixin_44755413/article/details/106095725
// CirclePerm(n,a)返回找到的最小圆排列长度。
// 初始时,数组a是输入的n个圆的半径,计算结束后返回相应于最优解的圆排列。
// Center计算当前所选择的圆在当前圆排列中圆心的横坐标,
// Compute计算当前圆排列的长度,
// 变量min记录当前最小圆排列的长度,数组r表示当前圆排列,
// 数组x则记录当前圆排列中各圆的圆心横坐标。
// 算法中约定在当前圆排列中排在第一个的圆的圆心横坐标为0.
class Circle
{
friend float CirclePerm(int,float *);
private:
float Center(int t);//计算当前所选择圆的圆心横坐标
void Compute(void);
void Backtrack(int t);
float min,//当前最优值
*x,//当前圆排列圆心横坐标
*r;//当前圆排列(可理解为半径排列)
int n;//待排列圆的个数
float Circle::Center(int t)
{
float valuex,temp = 0;
//之所以从1-t判断是因为防止第t个圆和第t-1个圆不相切
for(int j = 1;j < t;j++)
{
valuex = x[j] + sqrt(r[t] * r[j]);
if(valuex > temp)
temp = valuex;
}
return temp;
}
void Circle::Compute(void)
{
float low = 0,high = 0;
for(int i = 1;i <=n;i++)
{
if(x[i] - r[t] < low) low = x[i] - r[i];
if(x[i] + r[i] > high) high = x[i] + r[i];
}
if(high - low < min) min = high - low;
}
void Circle::Backtrack(int t)
{
if(t > n)
//到达叶子节点,我们计算high与low的差距
Compute();
else
{
//排列树解空间
for(int j = 1;j <= t;j++)
{
//圆的排列其实就是就是半径的排列,因为相同半径的圆是相同的
//交换半径顺序,可以进一步优化,如果半径相等不交换
//镜像序列只算一次,例如1,2,3和3,2,1
swap(r[t],r[j]);
if(Center(t)+r[1]+r[t] < min)//下界约束,我们取第一个圆的圆心为原点,所以计算距离的时候要加上r[1]和r[t]
{
x[t] = Center(t);
Backtrack(t+1;)
}
swap(r[t],r[j]);
}
}
}
float CirclePerm(int n,float *a)
{
Circle X;
X.n = n;
X.r = a;
X.min = 100000;
float *x = new float [n+1];//圆的中心坐标排列
X.x = x;
X.Backtrack(1);
delete[] x;
return X.min;
}
};
(二) 分支限界法
1、分枝限界法和回溯法
2、分枝限界法的基本思想
限界 —— 在结点扩展过程中,计算结点的上界,搜索的同时剪掉某些分支。
分支限界法就是把问题的可行解展开,再由各个分支寻找最佳解。
与回溯法类似,分支限界法也是在解空间树中搜索得到解;
不同的是,分支限界法会生成所有扩展结点,并舍弃不可能通向最优解的结点,然后根据广度优先/最小耗费优先,从活结点中选择一个作为扩展结点,使搜索向解空间上有最优解的分支推进
3、常见的两种分枝限界法
队列式(FIFO)分枝限界法
优先队列式分枝限界法
4、装载问题
(1) 队列式 分支限界法
(2)优先队列式 分支限界法
5、0-1背包问题
6、旅行售货员问题
最小堆
表示活结点优先队列。子树费用的下界lcost值
。每个顶点i的最小出边费用
并用min_out[i]记录有向图
中某个顶点没有出边
,则该图不可能有回路,算法结束。八、NP 理论
归约主要做的就是以下两个转化(注意两个转化都要在polynomial的时间内完成)【已知P是个NP-hard问题,证新问题Q 亦是NP-hard问题】,
问题的多项式变换(规约)
多项式变换(规约)的作用
NP-hard类问题的求解方法
九、近似算法
1、装箱问题(Bin Packing)
(1) First-Fit(FF)算法
(2) Next-Fit(NF)算法
(3) Best-Fit(BF)算法
(4)FFD(First-Fit Decreasing)算法
2、顶点覆盖问题
VertexSet approxVertexCover ( Graph g ) {
cset = 空集 ; // Cset用来存储顶点覆盖中的各顶点
e1 = g.e ;
while (e1 != 空集) {
从e1中任取一条边(u,v) ;
cset = cset∪{u,v} ;
从e1中删去与u和v相关联的所有边 ;
}
return c;
}
3、旅行商问题
1、装箱问题(Bin Packing)
(1) First-Fit(FF)算法
(2) Next-Fit(NF)算法
(3) Best-Fit(BF)算法
(4)FFD(First-Fit Decreasing)算法
2、顶点覆盖问题
VertexSet approxVertexCover ( Graph g ) {
cset = 空集 ; // Cset用来存储顶点覆盖中的各顶点
e1 = g.e ;
while (e1 != 空集) {
从e1中任取一条边(u,v) ;
cset = cset∪{u,v} ;
从e1中删去与u和v相关联的所有边 ;
}
return c;
}
3、旅行商问题