递归
算法设计或算法思维包括:递归算法、贪心算法、分治算法、动态规划、回溯算法等算法思维。(递贪分动回)
1.1 递归算法**(Recursion Algorithm)**
1.1.1 概念
递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于解决复杂的问题,让代码更简洁。
1.递归是一种非常高效、简洁的编码技巧,一种应用非常广泛的算法,比如DFS深度优先搜索、前中后序二叉树遍历等都是使用递归。
2.方法或函数调用自身的方式称为递归调用,调用称为递,返回称为归。
3.基本上,所有的递归问题都可以用递推公式来表示,比如
f(n) = f(n-1) + 1;
f(n) = f(n-1) + f(n-2);
f(n) = n * f(n-1);
递归本质
递归,去的过程叫"递",回来的过程叫”归“ 。
递是调用,归是结束后回来。递归是一种循环,而且在循环中执行的就是调用自己。
递归调用将每次返回的结果存在栈帧中。
1.1.2 什么样的问题可以用递归解决呢?
一个问题只要同时满足以下3个条件,就可以用递归来解决:
1.问题的解可以分解为几个子问题的解。何为子问题?就是数据规模更小的问题。
2.问题与子问题,除了数据规模不同,求解思路完全一样
3.存在递归终止条件
递归三要素
-
递归结束条件
既然是循环就必须要有结束,不结束就会OOM了。只递不归,程序崩溃。
-
函数的功能
这个函数要干什么,打印,计算....
-
函数的等价关系式
递归公式,一般是每次执行之间,或者与个数之间的逻辑关系
1.1.3 递归经典案例
经典案例:斐波那契数列:0、1、1、2、3、5、8、13、21、34、55.....
规律:从第3个数开始,每个数等于前面两个数的和。
递归分析:
-
函数的功能:返回n的前两个数的和
-
递归结束条件:从第三个数开始,n<=2
-
函数的等价关系式:fun(n)=fun(n-1)+fun(n-2)
如何实现递归?
1.递归代码编写
写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
2.递归代码理解
对于递归代码,若试图想清楚整个递和归的过程,实际上是进入了一个思维误区。
那该如何理解递归代码呢?如果一个问题A可以分解为若干个子问题B、C、D,你可以假设子问题B、C、D已经解决。而且,你只需要思考问题A与子问题B、C、D两层之间的关系即可,不需要一层层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。屏蔽掉递归细节,这样子理解起来就简单多了。因此,理解递归代码,就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。递归的关键是终止条件。
1.1.4 递归算法**核心代码**
1、斐波那契数列
package com.qf.algorithm;
class RecursionDemo {
public static void main(String[] args) {
// 放在要检测的代码段前,取开始前的时间戳
Long startTime = System.currentTimeMillis();
//1、斐波那契数列递归算法
int n = fiboRecursion(45);
System.out.println(n);
//2、斐波那契数列普通做法
//int m = fiboCommon(1);
//System.out.println(m);
// 放在要检测的代码段后,取结束后的时间戳
Long endTime = System.currentTimeMillis();
System.out.println("花费时间" + (endTime - startTime) + "ms");
}
//递归实现。寻找斐波那契数列中的第n个数值(索引下标从0开始)。
// 0、1、1、2、3、5、8、13、21、34、55.....
public static int fiboRecursion(int n) {
if (n <= 1) return n;
return fiboRecursion(n - 1) + fiboRecursion(n - 2);
}
//用非递归方式实现。寻找斐波那契数列中的第n个数值(索引下标从0开始)。
public static int fiboCommon(int n) {
if (n <= 1) return n;
int first = 0;
int second = 1;
int num = 0;
for (int i = 2; i <= n; i++) {
num = first + second;
first = second;
second = num;
}
return num;
}
}
运行结果:
递归算法的返回结果:
1134903170
花费时间4892ms
非递归算法的返回结果:
1134903170
花费时间0ms
2、求阶乘
经典的递归是求阶乘,如果要算出n!,只需要先算出(n-1)! ,再乘n就可以了(备注:递归求阶乘其实是相当差的一个方法)
public static int factorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("参数错误!");
}
if (n == 0) {
return 1;
} else {
return factorial(n - 1) * n;
}
}
从图中还可以看出来,每一次递归调用,都是彻底执行完这一次调用,才会返回。
1.1.5 递归案例
(一)、汉诺塔问题
1、汉诺塔问题的由来
汉诺塔问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
2、汉诺塔问题的解题思路
这个问题似乎十分困难,但利用递归的思想可以轻易解决:
首先,我们用一个这样的二元组表示一次从x到y的挪动(我们每次只能挪动x柱上最上面的圆盘,所以无需额外记录被挪动圆盘的编号),用若干个这样的组合表示一个挪动方案,那么,首先可以注意到的是,不论挪动方案是什么,其中必定有一步是,表示把最大的圆盘从A挪到C上,这一步显然是必需的,那么为了挪出这一步,首先A柱上只能有最大的圆盘,然后C柱必须为空,此时剩下的N-1个圆盘都在B柱上,因为大小的限制,它们的顺序也被确定了。于是在挪最大圆盘之前,就是把N-1个盘子从A挪到B,挪完最大盘子之后,就是再把N-1个盘子从B挪到C上。
2个盘(偶数):
A -> B
A -> C
B -> C
移动步数:3
*******
4个盘(偶数):
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C
移动步数:15
*******
6个盘(偶数):
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
C -> A
B -> C
B -> A
C -> A
C -> B
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C
B -> A
C -> A
C -> B
A -> B
C -> A
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C
移动步数:63
*******
3个盘(奇数):
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
移动步数:7
*******
5个盘(奇数):
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
B -> C
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
移动步数:31
*******
规律:
-
如果盘的个数是偶数,第一步永远是A -> B,最后一步永远是B -> C。
-
6个盘的前15步就是4个盘的所有步数,4个盘的前3步就是2个盘的全部步数。
-
如果盘的个数是奇数,第一步永远是A -> C,最后一步永远是A -> C。
-
7个盘的前31步就是5个盘的所有步数,5个盘的前7步就是3个盘的全部步数。
3、完整实现代码
package com.qf.algorithm;
/**
* 汉诺塔问题是源于印度一个古老传说的益智玩具。
* 大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。
* 大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。
* 并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
* 移动次数2^n-1,如果是64个圆盘,即便一秒移动一个,也需要5849亿年。
*/
public class HanoiTower {
public static void main(String[] args) {
HanoiTower ht = new HanoiTower();
int n = 3;
ht.move(n, 'A', 'B', 'C');
System.out.println("移动步数:" + ((1 << n) -1));
}
/**
* 程序用到了递归的思想。当只有1个盘时,只需直接从A移到C即可。
* 当有2个盘时,过程如下:A->B A->C B->C
* 总结规律,当n>=2时,
* 先将上面的 n-1 个盘由 A 借助 C 移到 B
* 然后最底下的一个大盘由 A 借助 B 移到 C (实际上就是直接从 A 移到 C)
* 最后将 B 上的 n-1 个盘由 B借助 A 移到 C
***
* 移动奇数个,最上面一个先放目标位置;移动偶数个,最上面一个先放非目标位置
* 移动次数:2^n - 1
* 数 a 向右移一位,相当于将 a 除以 2;数 a 向左移一位,相当于将 a 乘以 2
* @param n 盘子个数
* @param a**、b、c 将a柱上的盘子借助b柱,移动到c柱
*/
public void move(int n, char a, char b, char c) {
if (n == 1) {
System.out.println(a + " -> " + c);
} else {
// 实际上汉诺塔归根到底就是三步
// 将n-1个盘子从a移动到b
move(n - 1, a, c, b);
// 将最底下的1个盘子从a移动到c
move(1, a, b, c);
// 将n-1个盘子从b移动到c
move(n - 1, b, a, c);
}
}
}
运行结果:
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
移动步数:7
1.1.5 时间复杂度
斐波那契数列的普通递归解法为O(n²) ???
非递归算法为O(n)
递归算法的时间复杂度本质上是要看:递归的次数 * 每次递归中的操作次数
1.1.6 为什么使用递归?**递归的优缺点**
-
优点:代码的表达力很强,写起来简洁。
-
缺点:占用空间较大、如果递归太深,可能会发生栈溢出,可能会有重复计算,过多的函数调用会耗时较多等问题。通过备忘录或递归的方式 去优化(动态规划)
-
如何将递归改写为非递归代码?
笼统的讲,所有的递归代码都可以改写为迭代循环的非递归写法。如何做?抽象出递推公式、初始值和边界条件,然后用迭代循环实现。
1.1.7 递归常见问题及解决方案
1.警惕堆栈溢出:可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。
2.警惕重复计算:通过某种数据结构来保存已经求解过的值,从而避免重复计算。
1.1.8 递归的应用
-
递归作为基础算法,应用非常广泛,比如在二分查找、快速排序、归并排序、树的遍历上都有使用递归。
-
回溯算法、分治算法、动态规划中也大量使用递归算法实现。
1.2 贪心算法**(Greedy Algorithm)**
1.2.1 概念
贪心算法(Greedy),或贪婪算法。是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法。
贪心算法:当下做局部最优判断,不能回退 (能回退的是回溯,最优+回退是动态规划)。
由于贪心算法的高效性以及所求得答案比较接近最优结果,贪心算法可以作为辅助算法或解决一些要求结果不特别精确的问题。
用贪心法解题很方便,但它的适用范围很小,判断一个问题是否适合用贪心法求解,目前还没有一个通用的方法,在信息学竞赛中,需要凭个人的经验来判断。
注意:当下是最优的,并不一定全局是最优的。举例如下:
1、有硬币分值为10、9、4若干枚,问如果组成分值18,最少需要多少枚硬币?
采用贪心算法,选择当下硬币分值最大的:10
18-10=8
8/4=2
即:1个10、2个4,共需要3枚硬币。
实际上我们知道,选择分值为9的硬币,2枚就够了 18/9=2。
2、有硬币分值为10、5、1若干枚,问如果组成分值16,最少需要多少枚硬币?
采用贪心算法,选择当下硬币分值最大的:10
16-10=6
6-5=1
即:1个10,1个5,1个1 ,共需要3枚硬币,即为最优解。
由此可以看出贪心算法适合于一些特殊的情况,如果能用,一定是最优解。贪心策略一旦经过证明成立后,它就是一种高效的算法。比如,求最小生成树的Prim算法和Kruskal算法都是漂亮的贪心算法。
1.2.2 经典问题:背包问题
背包问题是算法的经典问题,分为部分背包和01背包,主要区别如下:
-
部分背包:某件物品是一堆,可以带走其一部分;
-
01背包:对于某件物品,要么被带走,要么不被带走,不存在只带走一部分的情况。
部分背包问题可以用贪心算法求解,且能够得到最优解。
假设一共有N件物品,第 i 件物品的价值为 Vi ,重量为Wi,一个小偷有一个最多只能装下重量为W的背包,他希望带走的物品越有价值越好,可以带走某件物品的一部分,请问他应该选择哪些物品?
假设背包可容纳50Kg的重量,物品信息如下表:
物品 | 重量(kg) | 价值(元) | 单位重量的价值(元/kg) |
---|---|---|---|
A | 10 | 60 | 6 |
B | 20 | 100 | 5 |
C | 30 | 120 | 4 |
贪心算法的关键是贪心策略的选择
将物品按单位重量所具有的价值排序。总是优先选择单位重量下价值最大的物品。
按照我们的贪心策略,单位重量的价值排序: 物品A > 物品B > 物品C
因此,我们尽可能地多拿物品A,直到将物品A拿完之后,才去拿物品B,然后是物品C。可以只拿一部分。
1.2.3 代码展示
1、部分背包问题
public class Goods {
String name;
double weight;
double price;
double val;
public Goods(String name, double weight, double price) {
this.name = name;
this.weight = weight;
this.price = price;
val = price / weight;
}
}
/**
* 贪心算法:部分背包问题
* 最大允许装50kg,怎么装能最大价值?
* 允许只拿一部分物品
*/
public class KnapsackAlgorithm {
// 包的最大容量,单位kg
double maxSize = 50;
public static void main(String[] args) {
KnapsackAlgorithm knapsack = new KnapsackAlgorithm();
Goods goods1 = new Goods("A", 10, 68);
Goods goods2 = new Goods("B", 20, 125);
Goods goods3 = new Goods("C", 30, 210);
Goods[] goodslist = {goods1, goods2, goods3};
knapsack.pack(goodslist);
}
// 装包
public void pack(Goods[] goodslist) {
// 对物品按照价值排序从高到低
Goods[] goodslist2 = sort(goodslist);
double weightSum = 0;
double priceSum = 0;
//取出价值最高的
for (int i = 0; i < goodslist2.length; i++) {
weightSum += goodslist2[i].weight;
if (weightSum <= maxSize) {
priceSum += goodslist2[i].unitPrice * goodslist2[i].weight;
System.out.println(goodslist2[i].name + "取" + goodslist2[i].weight + "kg,单价" + goodslist2[i].unitPrice + ",价值" + goodslist2[i].unitPrice * goodslist2[i].weight);
} else {
//此时的weightSum是加了最后一种商品才超重的,所以要减去。
double temp = maxSize - (weightSum - goodslist2[i].weight);
priceSum += goodslist2[i].unitPrice * temp;
System.out.println(goodslist2[i].name + "再取" + temp + "kg,单价" + goodslist2[i].unitPrice + ",价值" + goodslist2[i].unitPrice * temp);
System.out.println("===总金额:" + priceSum);
return;
}
}
}
// 按物品单价排序。每kg价值排序 由高到低price/weight
private Goods[] sort(Goods[] goodslist) {
Comparator<Goods> comparator = new Comparator<Goods>() {
@Override
public int compare(Goods g1, Goods g2) {
//负整数、零或正整数分别代表第一个参数小于、等于或大于第二个参数。
if (g1.unitPrice < g2.unitPrice) {
return 1;
} else if (g1.unitPrice > g2.unitPrice) {
return -1;
}
return 0;
}
};
Arrays.sort(goodslist, comparator);
return goodslist;
}
}
运行结果:
C取30.0kg,单价7.0,价值210.0
A取10.0kg,单价6.8,价值68.0
B再取10.0kg,单价6.25,价值62.5
===总金额:340.5
2、01背包问题
如果不可以只拿一部分,该如何设计呢?
本案例中的贪心策略就是选择单价最高,单个物品重量轻的。
5A:总价值300
3A+B:总价值280
A+2B:总价值260
2A+C:总价值240
B+C:总价值220
3、集合覆盖问题——贪心算法解决方案
有n个广播台,每个台有其可以覆盖的地区,如果选择最小的广播台,让所有地区都可以接收到信号?
4、均分纸牌
有n堆纸牌,编号分别为1,2,…,n。在任一堆上取若干张纸牌然后移动。移牌的规则为:在编号为1上取的纸牌,只能移到编号为2的堆上;在编号为n的堆上取的纸牌,只能移到编号为n-1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
例如:4堆纸牌分别为:① 9, ② 8, ③ 17, ④ 6
移动三次可以达到目的:
左移:第2堆减1,第1堆增1
左移:第3堆减3,第2堆增3
右移:第3堆减4,第4堆增4
移动次数:3次,每堆10张。
/**
* [均分纸牌]有n堆纸牌,编号分别为1,2,…,n。
* 每堆上有若干张,但纸牌总数必为n的倍数.可以在任一堆上取若干张纸牌,然后移动。
* 移牌的规则为:在编号为1上取的纸牌,只能移到编号为2的堆上;在编号为n的堆上取的纸牌,只能移到编号为n-1的堆上;
* 其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
* 现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
*/
public class Greedy {
public static void main(String[] args) {
// 每堆的扑克牌数量
int[] cards = {8, 6, 15, 17, 23, 9};
int count = moveCards(cards);
System.out.println("移动次数:" + count);
System.out.println("最后每堆扑克牌数量:" + Arrays.toString(cards));
}
/**
* 均分纸牌
* 1.若cards[i] > avg,则将 cards[i]-avg 张从第i堆移动到第i+1堆;
* 2.若cards[i] < avg,则将 avg-cards[i] 张从第i+1堆移动到第i堆。
*/
public static int moveCards(int[] cards) {
//总牌数
int sum = 0;
for (int i = 0; i < cards.length; i++) {
sum += cards[i];
}
//每堆平均牌数
int avg = sum / cards.length;
//移动次数
int count = 0;
for (int i = 0; i < cards.length - 1; i++) {
if (cards[i] > avg) {
int moveCards = cards[i] - avg;
cards[i] -= moveCards;
cards[i + 1] += moveCards;
System.out.println("右移:第" + (i + 1) + "堆减" + moveCards + ",第" + (i + 2) + "堆增" + moveCards);
} else if (cards[i] < avg) {
int moveCards = avg - cards[i];
cards[i + 1] -= moveCards;
cards[i] += moveCards;
System.out.println("左移:第" + (i + 2) + "堆减" + moveCards + ",第" + (i + 1) + "堆增" + moveCards);
}
count++;
}
return count;
}
}
运行结果:
左移:第2堆减5,第1堆增5
左移:第3堆减12,第2堆增12
左移:第4堆减10,第3堆增10
左移:第5堆减6,第4堆增6
右移:第5堆减4,第6堆增4
移动次数:5
最后每堆扑克牌数量:[13, 13, 13, 13, 13, 13]
5、拼接数字
有n个正整数,将它们连接成一排,组成一个最大的多位整数
/**
* 有n个正整数,将它们连接成一排,组成一个最大的多位整数
*/
public static void joinNums() {
String str = "";
int[] nums = {31, 7, 51, 510};
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
String a = "" + nums[i] + nums[j];
String b = "" + nums[j] + nums[i];
if (a.compareTo(b) < 0) {
nums[i] = (nums[i] + nums[j] - (nums[j] = nums[i]));
}
}
}
System.out.println(Arrays.toString(nums));
for (int i = 0; i < nums.length; i++) {
str += nums[i];
}
System.out.println("拼接的数字为:" + str);
}
1.2.4 时间复杂度
在不考虑排序的前提下,贪心算法只需要一次循环,所以时间复杂度是O(n)。
1.2.5 优缺点
-
优点:性能高,能用贪心算法解决的往往是最优解。
-
缺点:在实际情况下能用的不多,用贪心算法解的往往不是最好的。
1.2.6 适用场景
针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据(局部最优而全局最优)。
大部分能用贪心算法解决的问题,贪心算法的正确性都是显而易见的,也不需要严格的数学推导证明。 在实际情况下,用贪心算法解决问题的思路,并不总能给出最优解。
1.3 分治算法**(Divide and Conquer Algorithm)**
1.3.1 概念
分治算法(divide and conquer)的核心思想其实就是四个字:分而治之 。
也就是将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解。
关于分治和递归的区别:
分治算法是一种处理问题的思想,递归是一种编程技巧。
分治算法的递归实现中,每一层递归都会涉及这样三个操作:
-
分解:将原问题分解成一系列子问题;
-
解决:递归地求解各个子问题,若子问题足够小,则直接求解;
-
合并:将子问题的结果合并成原问题。
比如:将字符串中的小写字母转化为大写字母
“abcde”转化为"ABCDE" 我们可以利用分治的思想将整个字符串转化成一个一个的字符处理。
1.3.2 分治算法经典问题代码实现
一、实现字母转大写
public static void main(String[] args) {
String str = "abcde";
System.out.println(toUpCase(str.toCharArray(), 0));
str.toUpperCase();
}
public static char[] toUpCase(char[] chs, int i) {
if (i >= chs.length) return chs;
chs[i] = toUpCaseUnit(chs[i]);
return toUpCase(chs, i + 1);
}
public static char toUpCaseUnit(char c) {
int n = c;
if (n < 97 || n > 122) {
return ' ';
}
return (char) Integer.parseInt(String.valueOf(n - 32));
}
二、求x的n次幂问题
1、一般解法
比如:2^10 ,2的10次幂一般的解法是循环10次
//2的10次幂,一般的解法是循环10次
public static int commonPower(int x, int n) {
int res = 1;
while (n >= 1) {
res *= x;
n--;
}
return res;
}
该方法的时间复杂度是:O(n) 。
2、采用分治法
2^10拆成
2^5 * 2^5
2^2 * 2^2 * 2 * 2^2 * 2^2 * 2
2^1 * 2^1 * 2^1 * 2^1 * 2 * 2^1 * 2^1 * 2^1 * 2^1 * 2
可以看到每次拆成n/2次幂,时间复杂度是O(logn)
//分治法的解法
public static int dividPower(int x, int n) {
//递归结束 任何数的1次方都是它本身
if (n == 1) {
return x;
}
//每次分拆成幂的一半
int half = dividPower(x, n / 2);
//偶数
if (n % 2 == 0) {
return half * half;
} else {
return half * half * x;
}
}
1.3.3 时间复杂度
根据拆分情况可以是O(n)或O(logn)。
1.3.4 优缺点
-
优势:将复杂的问题拆分成简单的子问题,解决更容易,另外根据拆分规则,性能有可能提高。
-
劣势:子问题必须要一样,用相同的方式解决。
1.3.5 适用场景
-
分治算法能解决的问题,一般需要满足下面这几个条件:
-
原问题与分解成的小问题具有相同的模式;
-
原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别。
-
具有分解终止条件,也就是说,当问题足够小时,可以直接求解;
-
可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了。
-
二分查找、快速排序、归并排序的思路就是分治法。
1.4 动态规划算法(**Dynamic Programming** Algorithm**)**
1.4.1 概念
动态规划(Dynamic Programming),是一种分阶段求解的方法。
核心思想:将大问题划分为小问题进行解决,从而一步步获得最优解的处理算法。
动态规划算法与分治算法类似,基本思想都是将待求解问题分解成若干子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
动态规划算法与分治法不同的是,适合于用动态规划求解的问题,经过分解得到子问题往往不是互相独立的。也就是一个子阶段的求解是建立在上一个子阶段的解的基础上进行进一步的求解。
动态规划可以通过填表的方式来逐步推进,得到最优解。
1.4.2 经典问题
斐波那契数列 优化递归:
通过上边的递归树可以看出在树的每层和上层都有大量的重复计算,可以把计算结果存起来,下次再用的时候就不用再计算了,这种方式叫记忆搜索,也叫做备忘录模式。
1、**斐波那契数列——递归分治+记忆搜索(备忘录)实现**
/**
* 斐波那契数列: 递归分治+记忆搜索(备忘录)
*/
//定义属性,用于存储每次的计算结果
static int[] seq = new int[50];
public static int fiboMemo(int n) {
if (n <= 1) return n;
//没有计算过则计算
if (seq[n] == 0) {
seq[n] = fiboMemo(n - 1) + fiboMemo(n - 2);
}
//计算过直接从数组中取出返回
return seq[n];
}
2、**斐波那契数列——自底向上递推实现**
if(i<2) 则:dp[0] =0; dp[1] = 1;
if(i>=2) 则: dp[i] = dp[i-1] + dp[i-2];
最优子结构: fibo[9] = finbo[8] + fibo[7];
边界:a[0]=0; a[1]=1;
dp方程:fibo[n] = fibo[n-1] + fibo[n-2];
/**
* 斐波那契数列:自底向上递推
*/
public static int fiboDynamic(int n) {
if (n <= 1) return n;
int seq[] = new int[n + 1];
seq[0] = 0;
seq[1] = 1;
int i = 0;
for (i = 2; i <= n; i++) {
seq[i] = seq[i - 1] + seq[i - 2];
}
return seq[i - 1];
}
3、01背包问题
假设你是个贪婪的小偷,背着可装35磅(1磅≈0.45千克)重东西的背包,在商场伺机盗窃各种可装入背包的商品。
你力图往背包中装入价值最高的商品,你会使用哪种算法呢?同样,你采取贪婪策略,这非常简单。
(1) 盗窃可装入背包的最贵商品。
(2) 再盗窃还可装入背包的最贵商品,以此类推。
可盗窃的商品有下面三种。
你的背包可装35磅的东西。音响最贵,你把它给偷了,但背包没有空间装其他东西了。你偷到了价值3000美元的东西。
如果不是偷音响,而是偷笔记本电脑和吉他,总价将为3500美元!
在这里,贪婪策略显然不能获得最优解。动态规划可以找出最优解。
/**
* 动态规划(Dynamic Programming)
* 核心思想:将大问题划分为小问题进行解决,从而一步步获得最优解的处理算法。
* 动态规划可以通过填表的方式来逐步推进,得到最优解。
* 动态规划算法:01背包问题
* 物品1:重20kg,价值120元
* 物品2:重10kg,价值100元
* 物品3:重30kg,价值200元
* 物品4:重20kg,价值150元
*/
public class KnapsackDP {
public static void main(String[] args) {
KnapsackDP knapsackDP = new KnapsackDP();
// 定义四种物品的重量
int[] weight = {0, 2, 1, 3, 2};
// 定义四种物品的价值
int[] value = {0, 12, 10, 20, 15};
// 背包最大的承载量(单位kg)
int maxSize = 4;
int
//展示动态规划的表格信息
System.out.println("背包承重从0到" + maxSize + ",能装的最大价值为:");
for (int i = 0; i < dp.length; i++) {
System.out.print(i + "\t\t");
for (int j = 0; j < dp[0].length; j++) {
System.out.print(dp
}
System.out.println();
}
// 显示背包的装载信息
showInfo(dp, weight, value, maxSize);
}
/**
* 生成动态规划表格
* 参数weight:物品1到物品n的重量,其中weight[0] = 0
* 参数value:物品1到物品n的价值,其中value[0] = 0
* 功能:返回背包承载量从0到最大承载量所装物品的最大价值
*/
public int
// db的第一维下标是物品数量,第二维是从0到maxSize不同的承载量
int
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= maxSize; j++) {
if (j < weight[i]) {
dp
} else {
if (dp
dp
} else {
dp
}
}
}
}
return dp;
}
/**
* 对于动态规划的表格进行说明
*/
public static void showInfo(int
System.out.println("总价值:" + dp
System.out.println("分别是:");
int count = weight.length - 1;
for (int i = count; i > 0; i--) {
if (dp
System.out.println(i + "号物品,重量:" + weight[i] + ",价值" + value[i]);
maxSize -= weight[i];
}
}
}
}
运行结果:
背包承重从0到4,能装的最大价值为:
0 0 0 0 0 0
1 0 0 12 12 12
2 0 12 12 12 15
3 0 12 22 22 30
4 0 12 22 30 30
int[] weight = {0, 2, 1, 3, 2};
// 定义四种物品的价值
int[] value = {0, 12, 10, 20, 15};
总价值:30
分别是:
3号物品,重量:3,价值20
2号物品,重量:1,价值10
1.4.3 使用动态规划四个步骤
\1. 把当前的复杂问题转化成一个个简单的子问题(分治)
\2. 寻找子问题的最优解法(最优子结构)
\3. 把子问题的解合并,存储中间状态
\4. 递归+记忆搜索或自底而上的形成递推方程(dp方程)
1.4.4 时间复杂度
新的斐波那契数列实现时间复杂度为O(n)。
1.4.5 优缺点
-
优点:时间复杂度和空间复杂度都相对较低
-
缺点:难,有些场景不适用
1.4.6 适用场景
尽管动态规划比回溯算法高效,但是并不是所有问题,都可以用动态规划来解决。
能用动态规划解决的问题,需要满足三个特征:最优子结构、无后效性和重复子问题。
在重复子问题这一点上,动态规划和分治算法的区分非常明显。分治算法要求分割成的子问题,不能有重复子问题,而动态规划正好相反,动态规划之所以高效,就是因为回溯算法实现中存在大量的重复子问题。
1.5 回溯算法**(Backtracking Algorithm)**
1.5.1 概念
回溯法,又被称为“试探法”。解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯法。
回溯的处理思想,有点类似枚举(列出所有的情况)搜索。枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。
1.5.2 回溯VS递归
很多人认为回溯和递归是一样的,其实不然。在回溯法中可以看到有递归的身影,但是两者是有区别的。
回溯法从问题本身出发,寻找可能实现的所有情况。和穷举法的思想相近,不同在于穷举法是将所有的情况都列举出来以后再一一筛选,而回溯法在列举过程如果发现当前情况根本不可能存在,就停止后续的所有工作,返回上一步进行新的尝试。
递归是从问题的结果出发,例如求 n!,要想知道 n!的结果,就需要知道 n(n-1)! 的结果,而要想知道 (n-1)! 结果,就需要提前知道 (n-1)(n-2)!。这样不断地向自己提问,不断地调用自己的思想就是递归。
回溯和递归唯一的联系就是,回溯法可以用递归思想实现。
1.5.3 经典案例
1、八皇后问题
八皇后问题研究的是如何将8个皇后放置在 8×8 的棋盘上,并且使皇后彼此之间不能相互攻击。
我们把这个问题划分成 8 个阶段,依次将 8 个棋子放到第一行、第二行、第三行......第八行。
在放置的过程中,我们不停地检查当前放法,是否满足要求。如果满足,则跳到下一行继续放置棋子;如果不满足,那就再换一种放法,继续尝试。
解题思路:
1、第一个皇后先放在第一行第一列;
2、第二个皇后放在第二行第一列,然后判断是否OK,如果不ok,继续放在第二列、第三列,依次放置,直到找到合适位置;
3、第三个皇后放在第三行第一列,然后判断是否OK,如果不ok,继续放在第二列、第三列,依次放置,直到找到合适位置;
八皇后问题是使用回溯法解决的典型案例。算法的解决思路是:
-
从棋盘的第一行开始,从第一个位置开始,依次判断当前位置是否能够放置皇后,判断的依据为:同该行之前的所有行中皇后的所在位置进行比较,如果在同一列,或者在同一条斜线上(斜线有两条,为正方形的两个对角线),都不符合要求,继续检验后序的位置。
-
如果该行所有位置都不符合要求,则回溯到前一行,改变皇后的位置,继续试探。
-
如果试探到最后一行,所有皇后摆放完毕,则直接打印出 8*8 的棋盘。
1.5.3 代码实现
(一)、八皇后问题
/**
* 回溯算法:八皇后问题
* 共92种解法
*/
public class NQueens {
//皇后数
final static int N = 8;
//所有可能的摆放方案数
private int count = 0;
//所有可能尝试的摆放次数
private int totalCount = 0;
//每一行的摆放坐标。数组索引下标指棋盘的第几行,元素的值是每行中Queen摆放在第几列。
int[] singleResult = new int[N];
//记录所有的摆放方案
List<String> result = new ArrayList<>();
public static void main(String[] args) {
NQueens queens = new NQueens();
queens.putQueenAtRow(0);
System.out.println("摆放的方案总数:" + queens.count);
System.out.println("摆放的尝试总数:" + queens.totalCount);
// 每种摆放方案的棋子坐标
for (String data : queens.result) {
System.out.println(data);
}
}
//在每行放置Queen
public void putQueenAtRow(int row) {
//递归中断
if (row == N) {
count++;
//将每行的摆放方案存放到集合中
result.add(Arrays.toString(singleResult));
return;
}
//向这一行的每一个位置尝试排放皇后,然后检测状态,如果安全则继续执行递归,摆放下一行皇后。没有合适的则回到上一层
for (int col = 0; col < N; col++) {
totalCount++;
if (isSafety(row, col)) {
//存放当前安全的位置坐标
singleResult[row] = col;
//开始下一行
putQueenAtRow(row + 1);
}
}
}
// 判断将棋子放在第row行、第col列是否安全
private boolean isSafety(int row, int col) {
int step = 1;
// 第一行没有其他棋子,无需判断是否安全。
while (row - step >= 0) {
// 判断第col列是否有棋子
if (singleResult[row - step] == col) {
return false;
}
// 判断左上对角线是否有棋子
if (col - step >= 0 && singleResult[row - step] == col - step) {
return false;
}
// 判断右上对角线是否有棋子
if (col + step < N && singleResult[row - step] == col + step) {
return false;
}
step++;
}
return true;
}
//打印输出棋盘的图形
private void printChess() {
System.out.println("第 " + count + " 种解:");
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (singleResult[i] == j) {
System.out.print(" " + j + " ");
} else {
System.out.print(" + ");
}
}
System.out.println();
}
System.out.println("-----------------------");
}
}
运行结果:
摆放的方案总数:92
摆放的尝试总数:15720
[0, 4, 7, 5, 2, 6, 1, 3]
[0, 5, 7, 2, 6, 3, 1, 4]
[0, 6, 3, 5, 7, 1, 4, 2]
[0, 6, 4, 7, 1, 3, 5, 2]
[1, 3, 5, 7, 2, 0, 6, 4]
[1, 4, 6, 0, 2, 7, 5, 3]
[1, 4, 6, 3, 0, 7, 5, 2]
[1, 5, 0, 6, 3, 7, 2, 4]
[1, 5, 7, 2, 0, 3, 6, 4]
[1, 6, 2, 5, 7, 4, 0, 3]
[1, 6, 4, 7, 0, 3, 5, 2]
[1, 7, 5, 0, 2, 4, 6, 3]
[2, 0, 6, 4, 7, 1, 3, 5]
[2, 4, 1, 7, 0, 6, 3, 5]
[2, 4, 1, 7, 5, 3, 6, 0]
[2, 4, 6, 0, 3, 1, 7, 5]
[2, 4, 7, 3, 0, 6, 1, 5]
[2, 5, 1, 4, 7, 0, 6, 3]
[2, 5, 1, 6, 0, 3, 7, 4]
[2, 5, 1, 6, 4, 0, 7, 3]
[2, 5, 3, 0, 7, 4, 6, 1]
[2, 5, 3, 1, 7, 4, 6, 0]
[2, 5, 7, 0, 3, 6, 4, 1]
[2, 5, 7, 0, 4, 6, 1, 3]
[2, 5, 7, 1, 3, 0, 6, 4]
[2, 6, 1, 7, 4, 0, 3, 5]
[2, 6, 1, 7, 5, 3, 0, 4]
[2, 7, 3, 6, 0, 5, 1, 4]
[3, 0, 4, 7, 1, 6, 2, 5]
[3, 0, 4, 7, 5, 2, 6, 1]
[3, 1, 4, 7, 5, 0, 2, 6]
[3, 1, 6, 2, 5, 7, 0, 4]
[3, 1, 6, 2, 5, 7, 4, 0]
[3, 1, 6, 4, 0, 7, 5, 2]
[3, 1, 7, 4, 6, 0, 2, 5]
[3, 1, 7, 5, 0, 2, 4, 6]
[3, 5, 0, 4, 1, 7, 2, 6]
[3, 5, 7, 1, 6, 0, 2, 4]
[3, 5, 7, 2, 0, 6, 4, 1]
[3, 6, 0, 7, 4, 1, 5, 2]
[3, 6, 2, 7, 1, 4, 0, 5]
[3, 6, 4, 1, 5, 0, 2, 7]
[3, 6, 4, 2, 0, 5, 7, 1]
[3, 7, 0, 2, 5, 1, 6, 4]
[3, 7, 0, 4, 6, 1, 5, 2]
[3, 7, 4, 2, 0, 6, 1, 5]
[4, 0, 3, 5, 7, 1, 6, 2]
[4, 0, 7, 3, 1, 6, 2, 5]
[4, 0, 7, 5, 2, 6, 1, 3]
[4, 1, 3, 5, 7, 2, 0, 6]
[4, 1, 3, 6, 2, 7, 5, 0]
[4, 1, 5, 0, 6, 3, 7, 2]
[4, 1, 7, 0, 3, 6, 2, 5]
[4, 2, 0, 5, 7, 1, 3, 6]
[4, 2, 0, 6, 1, 7, 5, 3]
[4, 2, 7, 3, 6, 0, 5, 1]
[4, 6, 0, 2, 7, 5, 3, 1]
[4, 6, 0, 3, 1, 7, 5, 2]
[4, 6, 1, 3, 7, 0, 2, 5]
[4, 6, 1, 5, 2, 0, 3, 7]
[4, 6, 1, 5, 2, 0, 7, 3]
[4, 6, 3, 0, 2, 7, 5, 1]
[4, 7, 3, 0, 2, 5, 1, 6]
[4, 7, 3, 0, 6, 1, 5, 2]
[5, 0, 4, 1, 7, 2, 6, 3]
[5, 1, 6, 0, 2, 4, 7, 3]
[5, 1, 6, 0, 3, 7, 4, 2]
[5, 2, 0, 6, 4, 7, 1, 3]
[5, 2, 0, 7, 3, 1, 6, 4]
[5, 2, 0, 7, 4, 1, 3, 6]
[5, 2, 4, 6, 0, 3, 1, 7]
[5, 2, 4, 7, 0, 3, 1, 6]
[5, 2, 6, 1, 3, 7, 0, 4]
[5, 2, 6, 1, 7, 4, 0, 3]
[5, 2, 6, 3, 0, 7, 1, 4]
[5, 3, 0, 4, 7, 1, 6, 2]
[5, 3, 1, 7, 4, 6, 0, 2]
[5, 3, 6, 0, 2, 4, 1, 7]
[5, 3, 6, 0, 7, 1, 4, 2]
[5, 7, 1, 3, 0, 6, 4, 2]
[6, 0, 2, 7, 5, 3, 1, 4]
[6, 1, 3, 0, 7, 4, 2, 5]
[6, 1, 5, 2, 0, 3, 7, 4]
[6, 2, 0, 5, 7, 4, 1, 3]
[6, 2, 7, 1, 4, 0, 5, 3]
[6, 3, 1, 4, 7, 0, 2, 5]
[6, 3, 1, 7, 5, 0, 2, 4]
[6, 4, 2, 0, 5, 7, 1, 3]
[7, 1, 3, 0, 6, 4, 2, 5]
[7, 1, 4, 2, 0, 6, 3, 5]
[7, 2, 0, 5, 1, 4, 6, 3]
[7, 3, 0, 2, 5, 1, 6, 4]
(二)、迷宫问题
package com.qf.algorithm;
/**
* 利用递归思想模拟走迷宫
*/
public class MazePath {
public static void main(String[] args) {
int num = 10;
// 先创建一个二维数组
int
//使用1表示墙。设置外围是墙
for (int i = 0; i < num; i++) {
map
map
map
map
}
// 设置墙
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
map
// 输出地图
for (int i = 0; i < num; i++) {
for (int j = 0; j < num; j++) {
System.out.print(map
}
System.out.println();
}
//采用走法1
//findWay(map, 1, 1);
//采用走法2
findWay2(map, 1, 1);
// 输出行走轨迹
System.out.println("输出行走轨迹");
for (int i = 0; i < num; i++) {
for (int j = 0; j < num; j++) {
System.out.print(map
}
System.out.println();
}
System.out.println("步数:" + getCount(map));
}
/**
* @param map 地图
* @param i 表示出发点的Y轴坐标
* @param j 表示出发点的X轴坐标
* @return 约定map
* 按照右、下、左、上的方向走,如果走不通则回溯
*/
public static boolean findWay(int
if (map
System.out.println("走到出口!");
return true;
} else {
if (map
//假定可以走通,则标记为2,说明已经走过
map
if (findWay(map, i, j + 1)) {//向右走
return true;
} else if (findWay(map, i + 1, j)) {//向下走
return true;
} else if (findWay(map, i, j - 1)) {//向左走
return true;
} else if (findWay(map, i - 1, j)) {//向上走
return true;
} else {
// 说明此路不通
map
return false;
}
} else { //如果map
return false;
}
}
}
/**
* @param map 地图
* @param i 表示出发点的Y轴坐标
* @param j 表示出发点的X轴坐标
* @return 约定map
* 按照左下右上的方向走,如果走不通则回溯
*/
public static boolean findWay2(int
if (map
System.out.println("走到出口!");
return true;
} else {
if (map
//假定可以走通,则标记为2,说明已经走过
map
if (findWay2(map, i, j - 1)) {//向左走
return true;
} else if (findWay2(map, i + 1, j)) {//向下走
return true;
} else if (findWay2(map, i, j + 1)) {//向右走
return true;
} else if (findWay2(map, i - 1, j)) {//向上走
return true;
} else {
// 说明此路不通
map
return false;
}
} else { //如果map
return false;
}
}
}
// 计算走过的步数
public static int getCount(int
int count = 0;
for (int[] data : map) {
for (int ele : data) {
if (ele == 2 || ele == 3) {
count++;
}
}
}
return count;
}
}
打印结果:
步数:33
【备注:采用走法1的步数为36步,走法2的步数为33步】
(三)、马踏棋盘问题(骑士周游问题)
马踏棋盘算法也被称为骑士周游问题
将马随机放在过期象棋的8x8棋盘的某个方格中,马按走棋规则进行移动,要求每个方格只进入一次,走遍棋盘上全部64个方格。
骑士周游问题结局步骤和思路:
1.创建棋盘chessBoard,是一个二维数组
2.将当前位置设置为已个访问,然后根据当前位置,计算马儿还能走那些位置,并放到一个集合中(ArrayList),最多8个位置
3.变量ArrayList存放的所有位置,看看哪个可以走通
4.判断马儿是否完成了骑士周游问题
算法思想:对整个问题,考虑采用“回溯算法”与“贪心算法”两种算法来综合解决:
(1)回溯算法思想:搜索空间是整个棋盘上的8*8个点。约束条件是不出边界且每个点只能经过一次。搜索过程是从一点(i,j)出发,按深度有限的原则,从8个方向中尝试一个可以走的点,直到走过棋盘上所有的点.当没有点可达且没有遍历完棋盘时,就要撤销该点,从该点上一点开始找出另外的一个可达点,直到遍历完整个棋盘。
(2) 贪心算法思想:探讨每次选择位置的“最佳策略”,在确定马的起始节点后,在对其子结点进行选取时,优先选择出度最小的子节点进行搜索,这是一种局部调整最优的做法。如果优先选择出度多的子结点,那出度少的子结点就会越来越多,很可能出现‘死’结点(即没有出度又不能跳过的节点),反过来如果每次都优先选择出度少的结点跳,那出度少的结点就会越来越少,这样跳成功的机会就更大一些。
a) 先求出每个坐标点的出度值,即是该坐标下一步有几个方向可以走
b) 出度值越小,则被上一点选中的可能性就越大,下一个方向八个值的选择顺序保存MAP
public class HorseChessboard {
private static int X; // 棋盘的列数
private static int Y; // 棋盘的行数
//创建一个数组,标记棋盘的各个位置是否被访问过
private static boolean visited[];
//使用一个属性,标记是否棋盘的所有位置都被访问
private static boolean finished; // 如果为true,表示成功
public static void main(String[] args) {
System.out.println("骑士周游算法,开始运行~~");
//测试骑士周游算法是否正确
X = 8;
Y = 8;
int row = 1; //马儿初始位置的行,从1开始编号
int column = 1; //马儿初始位置的列,从1开始编号
//创建棋盘
int
visited = new boolean[X * Y];//初始值都是false
//测试一下耗时
long start = System.currentTimeMillis();
traversalChessboard(chessboard, row - 1, column - 1, 1);
long end = System.currentTimeMillis();
System.out.println("共耗时: " + (end - start) + " 毫秒");
//输出棋盘的最后情况
for (int[] rows : chessboard) {
for (int step : rows) {
System.out.print(step + "\t");
}
System.out.println();
}
}
/**
* 完成骑士周游问题的算法
***
* @param chessboard 棋盘
* @param row 马儿当前的位置的行 从0开始
* @param column 马儿当前的位置的列 从0开始
* @param step 是第几步 ,初始位置就是第1步
*/
public static void traversalChessboard(int
chessboard
//row = 4 X = 8 column = 4 = 4 * 8 + 4 = 36
visited[row * X + column] = true; //标记该位置已经访问
//获取当前位置可以走的下一个位置的集合
ArrayList<Point> ps = next(new Point(column, row));
//对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
sort(ps);
//遍历 ps
while (!ps.isEmpty()) {
Point p = ps.remove(0);//取出下一个可以走的位置
//判断该点是否已经访问过
if (!visited[p.y * X + p.x]) {//说明还没有访问过
traversalChessboard(chessboard, p.y, p.x, step + 1);
}
}
//判断马儿是否完成了任务,使用 step 和应该走的步数比较 ,
//如果没有达到数量,则表示没有完成任务,将整个棋盘置0
//说明: step < X * Y 成立的情况有两种
//1. 棋盘到目前位置,仍然没有走完
//2. 棋盘处于一个回溯过程
if (step < X * Y && !finished) {
chessboard
visited[row * X + column] = false;
} else {
finished = true;
}
}
/**
* 功能: 根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList), 最多有8种走法
***
* @param curPoint
* @return
*/
public static ArrayList<Point> next(Point curPoint) {
//创建一个ArrayList
ArrayList<Point> ps = new ArrayList<Point>();
//创建一个Point
Point p1 = new Point();
//表示马儿可以走5这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走6这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走7这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走0这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走1这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
//判断马儿可以走2这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿可以走3这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿可以走4这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
return ps;
}
//根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数
public static void sort(ArrayList<Point> ps) {
ps.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
//获取到o1的下一步的所有位置个数
int count1 = next(o1).size();
//获取到o2的下一步的所有位置个数
int count2 = next(o2).size();
if (count1 < count2) {
return -1;
} else if (count1 == count2) {
return 0;
} else {
return 1;
}
}
});
}
}
输出结果:
骑士周游算法,开始运行~~
共耗时: 25 毫秒
1 16 37 32 3 18 47 22
38 31 2 17 48 21 4 19
15 36 49 54 33 64 23 46
30 39 60 35 50 53 20 5
61 14 55 52 63 34 45 24
40 29 62 59 56 51 6 9
13 58 27 42 11 8 25 44
28 41 12 57 26 43 10 7
1.5.4 时间复杂度
八皇后问题的时间复杂度为: O(n!),实际为n!/2。
1.5.5 优缺点
-
优势:回溯算法的思想非常简单,大部分情况下,都是用来解决广义的搜索问题,也就是,从一组可能的解中,选择出一个满足要求的解。回溯算法非常适合用递归来实现,在实现的过程中,剪枝操作是提高回溯效率的一种技巧。利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率。
-
劣势:效率相对于动态规划算法低。
1.5.6 适用场景
回溯算法是个“万金油”。基本上能用动态规划算法、贪心算法解决的问题,都可以用回溯算法解决。
回溯算法相当于穷举搜索。穷举所有的情况,然后对比得到最优解。
不过,回溯算法的时间复杂度非常高,是指数级别的,只能用来解决小规模数据的问题。对于大规模数据的问题,用回溯算法解决的执行效率很低。
1.6 其他算法思维
1.6.1 分支限界法
1、概念
2、类型
1.6.2 概率算法
1、基本特征
2、分类
-
蒙特卡洛算法
-
拉斯维加斯算法
-
舍伍德算法
1.6.3 近似算法
1、概念
2、标准
1.6.4 数据挖掘算法
1、概述
2、过程
-
目标定义(任务理解、指标确定)
-
数据采集(建模抽样、质量把控、实时采集)
-
数据整理(数据探索、数据清洗、数据变换)
-
构建模型(模式发现、构建模型、验证模型)
-
模型评价(设定评价标准、多模型对比、模型优化)
-
模型发布(模型部署、模型重构)
1.6.5 智能优化算法
-
概述
-
具体算法
-
人工神经网络
-
遗传算法
-
模拟退火算法
-
禁忌搜索算法
-
蚁群算法
-