参考:
https://www.cnblogs.com/xsyfl/p/6921687.html
简单介绍
分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
分治算法可以求解的一些经典问题
-
大整数乘法
-
二分搜索
-
棋盘覆盖
-
合并排序
-
快速排序
-
线性时间选择
-
最接近点对问题
-
循环赛日程表
-
汉诺塔
分治法能解决的问题具备的特征
分治法在每层递归上的步骤
-
分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
-
解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
-
合并:将各个子问题的解合并为原问题的解。
分治算法的设计模式:
假设:
-
|P|表示问题P的规模
-
n0表示分解阈值;即当问题P的规模不超过n0时,问题P不需要继续分解
-
ADHOC(Pk)表示分治法的基本子算法,用于直接解小规模的问题P;即当Pk的规模不超过n0时,直接用ADHOC(Pk)求解
-
MERGER(y1, y2, ......, yk)合并子算法,用于将P的子问题P1,P2,......,Pk的相应的解y1,y2,......,yk合并为P的解。
DivideAndConquer(P){
//归上来的起始条件
if |P| <= n0 :
then return ADHOC(P);
//将P分解为较小的子问题P1, P2, ... , Pk
//递下去
for i <- 1 to k :
do yi <- DivideAndConquer(Pi);
T <- MERGE(y1, y2, ... , yk);
return T;
}
分治法的复杂性分析
提前告知:假设
- 问题规模为n
- 分解为k个子问题
- 第i个子问题的规模为:n/(m^i),i ∈ [0, 1, ... , k]
- 分解阈值n0为1
- ADHOC( ) 解 规模为1的问题 需要耗费 1个单位时间
- 将原问题分解为k个子问题 以及 用merge将k个子问题的解合并为原问题的解 需要f(n)个单位时间。
- T(n)表示该分治法解规模为|P|=n的问题所需的计算时间
则一般情况下的计算时间为:
T(n) = f(n) + k*T(n / m)
这里假设:n = m^k,即可以分解到子问题的规模1,或者称:只给出n等于m的方幂时 递归方程T(n)的值
另外,如果T(n)曲线是光滑的(可求导),那么根据自变量n和因变量T(n)就可以估计T(n)的增长速度。
一般情况下,假设T(n)单调递增,从而当mi <= n < mi+1时,T(mi) <= T(n) < T(mi+1)。
最经典问题问题之一:汉诺塔
在汉诺塔游戏中,有三个分别命名为A、B、C得塔座,几个大小各不相同,从小到大一次编号得圆盘,每个原盘中间有一个小孔。最初,所有得圆盘都在A塔座上,其中最大得圆盘在最下面,然后是第二大,以此类推.
游戏的目的是将所有的圆盘从塔座A移动到塔座B,塔座C用来当作临时圆盘,游戏的规则如下:
- 在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
问题分析:
肯定是先把圆盘4放到柱子B,才能把圆盘3放到柱子B,才能把圆盘2放到柱子B,以此类推。
即,我们不能直接移动圆盘4,但是圆盘4最终将从塔座A移动到塔座B。按照游戏规则,在移动圆盘4之前的情况一定如下图:
所以,完成游戏的思路为:
先将①前三个圆盘从A移动到C,然后②圆盘4从A移动到B,最后③前三个圆盘从C移动到B。C为辅助
①前三个圆盘从A移动到C:a. 前两个从A移动到B,b. 圆盘3从A移动到C,c. 前两个圆盘从B移动到C。B为辅助
a. 前两个从A移动到B:第一个从A移动到C,圆盘2从A移动到B,第一个从C移动到B。C为辅助
c. 前两个从B移动到C:第一个从B移动到A,圆盘2从B移动到C,第一个从A移动到C。A为辅助
③前三个圆盘从C移动到B:前两个从C移动到A,第三个从C移动到B,前两个从A移动到B。A为辅助
注意:高亮的部分为分解的过程(递下去的过程),未高亮的部分为每层递归应该做的事情,归上来的起始条件为分解为一个圆盘的时候。
最终我们只需要处理一个圆盘从一个塔座移动到另一个塔座的问题。
/**
* @author wkn
* @create 2021-11-29 14:59
*/
public class DivideAndConquer {
public static int count = 1;
public static void main(String[] args) {
moved(4, "第一根柱子", "第二根柱子", "第三根柱子");
}
/**
* @param i 第几个圆盘
* @param a 圆盘所在位置
* @param b 圆盘要移动到的位置
* @param c 辅助位置
*/
public static void moved(int i, String a, String b, String c){
if(i == 1){
disPlay(i, a, b);
}else{
moved(i - 1, a, c, b);
disPlay(i, a, b);
moved(i - 1, c, b, a);
}
}
public static void disPlay(int i, String col1, String col2){
System.out.println("第" + count + "步: 将第" + i + "个圆盘从" + col1 + "移动到" + col2);
count++;
}
}
前奏:合并两个升序链表
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
ListNode hair = new ListNode();
hair.next = l1;
ListNode p1 = l1;
ListNode p1Pre = hair;
ListNode p2 = l2;
while (p1 != null && p2 != null) {
if(p2.val < p1.val) {
ListNode temp = p2.next;
p1Pre.next = p2;
p2.next = p1;
p2 = temp;
p1Pre = p1Pre.next;
}else {
p1Pre = p1Pre.next;
p1 = p1.next;
}
}
if(p2 != null) {
p1Pre.next = p2;
}
return hair.next;
}
}
困难题:合并K个升序链表
来源:合并k个升序链表
分治法:先分组再合并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
public ListNode merge(ListNode[] lists, int left, int right) {
if(left > right) {
return null;
}
if(left == right) {
return lists[left];
}
int mid = (left + right) >> 1;//位运算,除以2
ListNode leftListNode = merge(lists, left, mid);
ListNode rightListNode = merge(lists, mid + 1, right);
return mergeListNode(leftListNode, rightListNode);
}
public ListNode mergeListNode(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
ListNode hair = new ListNode();
hair.next = l1;
ListNode p1 = l1;
ListNode p1Pre = hair;
ListNode p2 = l2;
while (p1 != null && p2 != null) {
if(p2.val < p1.val) {
ListNode temp = p2.next;
p1Pre.next = p2;
p2.next = p1;
p2 = temp;
p1Pre = p1Pre.next;
}else {
p1Pre = p1Pre.next;
p1 = p1.next;
}
}
if(p2 != null) {
p1Pre.next = p2;
}
return hair.next;
}
}