##目录
- 分治法基本思想
- 分治法的使用前提
- 分治法的解题步骤
- 分治法的时间复杂度
- 棋盘覆盖问题
- 归并排序和快速排序
- 循环赛日程表
- 整数因子分解
- 半数集
###分治法基本思想
相关的基本知识:
- 就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
- 分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
- 分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
###分治法的使用前提
前提:
- 该问题的规模缩小到一定的程度就可以容易地解决
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
注意:
- 第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
###分治法的解题步骤
步骤
- 划分问题:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
- 递归求解:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
- 合并问题:将各个子问题的解合并为原问题的解。
###分治法的时间复杂度
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
T(n) = k*T(n/m) + f(n)。
###棋盘覆盖问题
棋盘覆盖问题:
在一个2^k * 2^k的方格中,由一个特殊棋盘,要你用四种不同形态的L型骨牌覆盖除了这个特殊棋盘以外的棋盘。
分治法求解思路:
特殊方格必定位于4个较小棋盘之中,其余3个子棋盘中无特殊方格。为了这三个无特殊棋盘的子棋盘转换为特殊棋盘,我们可以用一个L型骨牌来覆盖这三个较小棋盘的汇合处(每次覆盖一个(相当于覆盖之后就变成了特殊棋盘)),然后原问题转换成四个较小的子问题,递归的使用这种分割,知道棋盘转换成11的棋盘。
时间复杂度分析:
当k = 0(k就是2^k中的k)时,T(k) = O(1); 当k大于0时T(k) = 4T(k-1) + O(1); 所以可以得到T(k) = O(4^k);
import java.io.BufferedInputStream;
import java.util.Scanner;
/**
* 利用分治法解决棋盘覆盖问题
* @author 郑鑫
*/
public class ChessBoard {
private static int title;
private static int[][] board;
//tr,tc棋盘左上角方格的行号和列号,dr,dc表示特殊方格的行号和列号
public static void chessBoard(int tr,int tc,int dr,int dc,int size){ //size表示的是2*k -->棋盘大小2*k * 2*k
if(size == 1)return;
int t = title++; //L型骨牌的编号
int s = size/2; //棋盘分割
if(dr < tr + s && dc < tc + s){ //特殊方格放在词棋盘中
chessBoard(tr, tc, dr, dc, s);
}else { //此棋盘中无特殊方格
board[tr+s-1][tc+s-1] = t; //在左上角棋盘的右下方填充一下
chessBoard(tr, tc, tr+s-1, tc+s-1, s);
}
if(dr < tr + s && dc >= tc + s){ //右上角棋盘
chessBoard(tr, tc+s, dr, dc, s);
}else { //特殊棋盘不在这个右上角子棋盘中
board[tr+s-1][tc+s] = t;
chessBoard(tr, tc+s, tr+s-1, tc+s, s);
}
if(dr >= tr + s && dc < tc + s){ //在左下角棋盘中
chessBoard(tr+s, tc, dr, dc, s);
}else {
board[tr+s][tc+s-1] = t;
chessBoard(tr+s, tc, tr+s, tc+s-1, s);
}
if(dr >= tr + s && dc >= tc + s){
chessBoard(tr+s, tc+s, dr, dc, s);
}else {
board[tr+s][tc+s] = t;
chessBoard(tr+s, tc+s, tr+s, tc+s, s);
}
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int size = cin.nextInt();
title = 1;
board = new int[size+1][size+1];
chessBoard(0,0, 0, 1, size);
System.out.println("------输出覆盖后的棋盘(特殊棋盘用0表示)-------");
for(int i = 0; i < size; i++){
for(int j = 0; j < size; j++){
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
}
运行效果
###归并排序和快速排序
这两个也是分治法很好的一个例子,具体的算法过程在各种排序算法总结已经分析过。这里不再细说。
###循环赛日程表
循环日程表问题。n=2^k个运动员进行网球循环赛,需要设计比赛日程表。每个选手必须与其他n−1个选手各赛一次;每个选手一天只能赛一次;循环赛一共进行n−1天。按此要求设计一张比赛日程表,该表有n行和n−1列,第i行j列为第i个选手第j天遇到的选手。
也是使用分治法解决,和棋盘覆盖问题有点像。
我们将所有选手对分成两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定,递归的用这种一根为二的策略对选手进行分割,直到只剩下两个选手时,比赛日程表的指定就变得很简单了,这时只要这两个选手进行比赛就可以了。
import java.io.BufferedInputStream;
import java.util.Scanner;
/**
* 用分治法解决循环日程表安排问题
* @author 郑鑫
*/
public class CirculateSchedule {
private static int[][] map;
// 将2^k * 2^k的表格分成2^(k-1)*2^(k-1)的四个子表格
// 每个表格的左上角赋值
// 左上子表格等于右下子表格,右上子表格等于左下子表格
// 右上子表格等于左上子表格加上子表格大小
public static void Table(int r,int c,int size){
if(size == 1)return;
int half = size/2;
map[r + half][c + half] = map[r][c];
map[r+half][c] = map[r][c+half] = map[r][c]+half;
Table(r, c, half);
Table(r, c+half, half);
Table(r+half, c, half);
Table(r+half, c+half, half);
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n = cin.nextInt();
map = new int[n+1][n+1];
map[1][1] = 1;
Table(1, 1, n);
for(int i = 1; i <= n; i++){
System.out.print(i + " ");
for(int j = 2; j <= n; j++){
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
效果
###整数因子分解
整数因子分解问题
问题描述:
大于1 的正整数n 可以分解为:n=x1 x 2…xm 。 例如,当n= 12
时,共有8 种不同的分解式: 12= 12; 12=62; 12=43; 12=34; 12=322; 12=26;
12=232; 12=22*3。 对于给定的正整数n,编程计算n 共有多少种不同的分解式。
数据输入:
输入数据第一行有1 个正整数n (1≤n≤2000000000) 。
结果输出:
将计算出的不同的分解式数。
输入 : 12 输出 : 8
解题思路 : 递归所有可能被分解的因子,然后一次除尽到1总的个数就加一。
import java.io.BufferedInputStream;
import java.util.Scanner;
/**
* 整数因子分解: 递归分治
* @author 郑鑫
*/
public class Factoring {
private static int sum;
public static void solve(int n){
if(n == 1) sum ++;
else for(int i = 2; i <= n; i++)if(n % i == 0)solve(n/i);
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n = cin.nextInt();
sum = 0;
solve(n);
System.out.println(sum);
}
}
###半数集
问题描述:
给定一个自然数n,由n 开始可以依次产生半数集set(n)中的数如下。
(1) n∈set(n);
(2) 在n 的左边加上一个自然数,但该自然数不能超过最近添加的数的一半;
(3) 按此规则进行处理,直到不能再添加自然数为止。
例如,set(6)={6,16,26,126,36,136}。半数集set(6)中有6 个元素。
注意半数集是多重集。
解题思路:也是运用递归分治的思想,很容易看出以下公式:
set(n) = set(i)(i : 1~n/2) + 1;然后根据递归的思想求解即可。
这里可以设置一个标记数组避免计算重复的子问题(计算重复的子问题的和)
import java.io.BufferedInputStream;
import java.util.Scanner;
/**
* 半数集问题
* @author 郑鑫
*/
public class CompSet {
private static long[] vis;
public static long comp(int n){
long ans = 1;
if(vis[n] > 0)return vis[n];
for(int i = 1; i <= n/2; i++)ans += comp(i);
vis[n] = ans;
return ans;
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n = cin.nextInt();
vis = new long[n+1];
for(int i = 0; i <= n; i++)vis[i] = 0; //先规0
System.out.println(comp(n));
}
}