一、分治法
基本思想:分治法就是将问题分为一个一个相同和相似的独立子问题,子问题分别解决后,最终在进行合并
主要性质:子问题相互独立、子问题可合并出原问题的解
常用实例:二分查找法、快速排序法、合并排序法
分治算法经典列题:
输入一组整数,求出这组数字子序列和中最大值,也就是求出最大子序列的和,不必求出最大的那个序列。例如:序列:-2、11、-1、13、-5、-2,则最大子序列的和为20.
public static void main(String[] args) {
int[] a = { -2, 11, -4, 13, -5, -2 };// 最大子序列和为20
int[] b = { -6, 2, 4, -7, 5, 3, 2, -1, 6, -9, 10, -2 };// 最大子序列和为16
System.out.println(maxSubSum1(a));
System.out.println(maxSubSum4(b));
}
// 最大子序列求和算法一
public static int maxSubSum1(int[] a) {
int maxSum = 0;
// 从第i个开始找最大子序列和
for (int i = 0; i < a.length; i++) {
// 找第i到j的最大子序列和
for (int j = i; j < a.length; j++) {
int thisSum = 0;
// 计算从第i个开始,到第j个的和thisSum
for (int k = i; k <= j; k++) {
thisSum += a[k];
}
// 如果第i到第j个的和小于thisSum,则将thisSum赋值给maxSum
if (thisSum > maxSum) {
maxSum = thisSum;
}
}
}
return maxSum;
}
// 时间复杂度O(n的平方)
public static int maxSubSum2(int[] a) {
int maxSum = 0;
for (int i = 0; i < a.length; i++) {
// 将sumMax放在for循环外面,避免j的变化引起i到j的和sumMax要用for循环重新计算
int sumMax = 0;
for (int j = i; j < a.length; j++) {
sumMax += a[j];
if (sumMax > maxSum) {
maxSum = sumMax;
}
}
}
return maxSum;
}
// 递归,分治策略
// 2分logn,for循环n,时间复杂度O(nlogn)
public static int maxSubSum3(int[] a) {
return maxSumRec(a, 0, a.length - 1);
}
public static int maxSumRec(int[] a, int left, int right) {
// 递归中的基本情况
if (left == right) {
if (a[left] > 0)
return a[left];
else
return 0;
}
int center = (left + right) / 2;
// 最大子序列在左侧
int maxLeftSum = maxSumRec(a, left, center);
// 最大子序列在右侧
int maxRightSum = maxSumRec(a, center + 1, right);
// 最大子序列在中间(左边靠近中间的最大子序列+右边靠近中间的最大子序列)
int maxLeftBorderSum = 0, leftBorderSum = 0;
for (int i = center; i >= left; i--) {
leftBorderSum += a[i];
if (leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}
int maxRightBorderSum = 0, rightBorderSum = 0;
for (int i = center + 1; i <= right; i++) {
rightBorderSum += a[i];
if (rightBorderSum > maxRightBorderSum)
maxRightBorderSum = rightBorderSum;
}
// 返回最大子序列在左侧,在右侧,在中间求出的值中的最大的
return max3(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum);
}
public static int max3(int a, int b, int c) {
return a > b ? (a > c ? a : c) : (b > c ? b : c);
}
// 时间复杂度:O(n); 任何a[i]为负时,均不可能作为最大子序列前缀;任何负的子序列不可能是最有子序列的前缀
public static int maxSubSum4(int[] a) {
int maxSum = 0, thisSum = 0;
for (int j = 0; j < a.length; j++) {
thisSum += a[j];
if (thisSum > maxSum)
maxSum = thisSum;
else if (thisSum < 0)
thisSum = 0;
}
return maxSum;
}
二、动态规划
基本思想:将问题分为一个一个阶段的子问题,未解决的子问题需要在已解决的子问题的基础上就得,动态规划一定会有一个边界。
主要性质:每个子问题都是全局最优结构。无后效性,某个状态只要确定便不受接下来决策的影响。有重叠的子问题。
标注:动态规划问题,一定要先找到递推方程,然后根据递推方程求解,逐渐推出最终结果
动态规划算法经典列题:
public static void main(String[] args) {
String A = "1A2C3D4B56";
String B = "B1D23CA45B6A";
System.out.println(findLCS(A, A.length(), B, B.length()));
}
public static int findLCS(String A, int Alen, String B, int Blen) {
int dp[][] = new int[Alen+1][Blen+1];
for (int i = 0; i < Alen; i++) {
for (int j = 0; j < Blen; j++) {
if (A.charAt(i) == B.charAt(j)) {
dp[i + 1][j + 1] = dp[i][j] + 1;
} else {
dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
return dp[Alen][Blen];
}
三、贪心算法
基本思想:将问题分为一个一个阶段的子问题,下一个问题的解决必须要在上一个问题解决的基础之上,从初始状态逐渐逼近最终状态。
主要性质:局部最优解能导致全局最优解、子问题无后效性
实际应用:
这个问题在我们的日常生活中就更加普遍了。假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。在日常生活中我们自然而然也是这么做的。在程序中已经事先将Value按照从小到大的顺序排好。
下面展示一些内联代码片。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=7;
int Count[N]={3,0,2,1,0,3,5};
int Value[N]={1,2,5,10,20,50,100};
int solve(int money)
{
int num=0;
for(int i=N-1;i>=0;i--)
{
int c=min(money/Value[i],Count[i]);
money=money-c*Value[i];
num+=c;
}
if(money>0) num=-1;
return num;
}
int main()
{
int money;
cin>>money;
int res=solve(money);
if(res!=-1) cout<<res<<endl;
else cout<<"NO"<<endl;
}
四、回溯算法
基本思想:回溯法又称为试探法,是一种优选搜索法,按选优条件向前搜索,以达到目标。回溯法采用试错的思想,它尝试分布的去解决一个问题。在分布解决问题的过程中,当它通过尝试发现现有的分布答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其他的可能的分布解答再次尝试寻找问题的答案。
常用实例:深度优先搜索、BF字符串匹配算法、KMP算法
常见列子:八皇后问题
该问题的描述是:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。该问题有多种解法,递归法是最简单的一种,本文使用递归法,来借这个问题来介绍回溯法。
如果是初次接触该问题,一看到这个题目可能会觉得手足无措。可以尝试着将题目转换为:在一个8x8的二维数组上,每次在每一行放置一个元素,使得这8行的元素互相不在同一行、同一列或同一斜线上。假如8x8棋盘原始的元素都是0,在某行某列放置皇后后,该位置的元素改为1。所以可以通过该列的8行是否存在元素1,来判断该列是否存在皇后。
package Recall;
/*
* 在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
*/
public class EightQueen {
/*总的可能数,初始化为0,每输出一次结果,自增1*/
private static int count=0;
/*创建一个8x8的棋盘,默认初始化元素为0,代表未放置皇后*/
private static int[][] chess=new int[8][8];
public static void main(String[] args) {
/*传参0,代表从第一行开始遍历,寻找放置皇后的位置*/
eightQueen(0);
System.out.println("八皇后问题总共有"+count+"种结果");
}
private static void eightQueen(int row){
/*如果遍历完八行都找到放置皇后的位置则打印*/
if(row>7){
printQueen();
count++;
return;
}
/*在每一行放置皇后,即遍历某行中的每一列*/
for(int col=0;col<8;col++){
/*判断是否可以放置皇后*/
if(isExistQueen(row,col)){
/*该位置放置皇后*/
chess[row][col]=1;
/*然后继续在下一行进行判断*/
eightQueen(row+1);
/*清零,这也是回溯法要注意的地方,一种方法尝试后,需要将之前做的尝试回退,以免影响到下一次尝试*/
chess[row][col]=0;
}
}
}
private static void printQueen(){
System.out.println("第 "+(count+1)+"种结果:");
for(int row=0;row<8;row++){
for(int col=0;col<8;col++){
/*放置皇后*/
if(chess[row][col]==1){
System.out.print("q ");
/*放置士兵*/
}else{
System.out.print("s ");
}
}
System.out.println();
}
System.out.println();
}
private static boolean isExistQueen(int row,int col){
int i,k;
/*判断同一列中是否存在1,即皇后*/
for(i=0;i<8;i++){
if(chess[i][col]==1)
return false;
}
/*判断从(0,0)到(i,k)区域内,即左上角的对角线位置上是否存在1,即皇后
*此时不检查左下角,是因为左下角对应的行还没开始放置皇后,不用检查*/
for(i=row,k=col;i>=0&&k>=0;i--,k--){
if(chess[i][k]==1)
return false;
}
/*判断从(0,7)到(i,k)区域内,即右上角的对角线位置上是否存在1,即皇后
*此时不检查右下角,是因为右下角对应的行还没开始放置皇后,不用检查*/
for(i=row,k=col;i>=0&&k<8;i--,k++){
if(chess[i][k]==1)
return false;
}
return true;
}
}
五、分支界限法
基本思想:分支界限法常以广度优先或以最小消耗优先的方式搜索问题的解空间树。
常用实例:广度优先搜索
举例:
# Author:Iron
# 初始化图参数 用字典初始初始化这个图
G = {1: { 2: 4, 3: 2,4:5},
2: { 5: 7, 6: 5},
3: {6: 9},
4: {5: 2, 7: 7},
5: {8: 4},
6: {10:6},
7: {9: 3},
8: {10:7},
9: {10:8},
10:{}
}
inf=9999
#保存源点到各点的距离,为了让顶点和下标一致,前面多了一个inf不用在意。
length=[inf,0,inf,inf,inf,inf,inf,inf,inf,inf,inf]
Q=[]
#FIFO队列实现
def branch(G,v0):
Q.append(v0)
dict=G[1]
while len(Q)!=0:
#队列头元素出队
head=Q[0]
#松弛操作,并且满足条件的后代入队
for key in dict:
if length[head]+G[head][key]<=length[key]:
length[key]=length[head]+G[head][key]
Q.append(key)
#松弛完毕,队头出列
del Q[0]
if len(Q)!=0:
dict=G[Q[0]]
'''
#优先队列法实现
def branch(G, v0):
Q.append(v0)
while len(Q) != 0:
min=99999
flag=0
#找到队列中距离源点最近的点
for v in Q:
if min > length[v]:
min=length[v]
flag = v
head = flag
dict=G[head]
#找到扩散点后进行松弛操作
for key in dict:
if length[head] + G[head][key] <= length[key]:
length[key] = length[head] + G[head][key]
Q.append(key)
#松弛完毕后,该扩散点出队
Q.remove(head)
'''
branch(G,1)
print(length)
六、梳理知识点
1.框架流程设计
框架流程设计是指对于一个软件或系统的整体结构和功能进行规划和组织的过程
2.高级编程技巧和优良的设计模式
高级编程技巧和优良的设计模式是在软件开发过程中用于提高代码质量、可维护性和可扩展性的重要工具
3.算法
算法是解决问题的一系列步骤和操作的有序集合。在计算机科学和编程中,算法是用来描述解决特定问题的步骤和逻辑的指令集合
4.构建、打包、发布
构建、打包和发布是软件开发过程中的重要步骤,用于将代码转化为可执行的软件,并将其交付给最终用户