分治,分治,分而治之。这句话应该是分治法的核心所在。
当给定一个问题之后,首先是观察最简单的实例。如果能够解决最简单的实例,那接下来思考能不能将大的实例分解为小的实例?能否将小实例组合形成大的实例的解?如果两个问题的答案都是能的话,那么可以说大的实例能够规约形成小的实例。通过连续执行归约操作,就能将原给定实例逐步归约成最简单的实例;然后再反方向操作,就能将最简单实例的解逐步“组合”成原给定实 例的解。
那么,如何判断能否将大的实例分解成小的实例呢?如果能的话,又该如何分解呢?我们可以通过观察问题形式化描述中“输入”部分的关键数据结构来获得一些线索:一个字符串的一部分仍然是字符串、一个集合的一部分仍然是集合、一棵树去除根节点之后会形成若干子树,以及一个图的一部分是一个子图。
进一步地,如何判断能否将小实例的解“组合”成大实例的解呢?如果能的话,又该如何组合呢?我们可以通过观察问题形式化描述中“输出”部分的形式和约束条件来获得一些线索。
因此,要想准确地判断规模较大的实例能够归约成规模较小的实例,我们既要考虑问题描述中的“输入”部分,也要考虑“输出”部分。如果能够归约的话,我们就称这个问题具有递归结构,并可以设计递归算法进行求解。而基于规约思想的算法中,最典型、最基础的就是分治法。
假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
假设数组中不存在重复元素。
示例1
输入: [3,4,5,1,2]
输出: 1
示例2
输入: [4,5,6,7,0,1,2]
输出: 0
这题最初始的想法就是遍历数组将最小值储存,然后挨个比较更新最小值,这个方法肯定不是最优解,因为输入的数组是有规律的。仔细观察会发现,将数组划分之后,会存在一个子数组是严格升序,另外一个数组同样是一个旋转排序数组,最小值必定会出现在旋转排序数组中。因此可以将问题逐步递归划分,直至子数组大小为1。
class Solution {
public int findMin(int[] nums) {
return func(nums, 0, nums.length - 1);
}
public static int func(int[] nums, int l, int r) {
if (l == r) return nums[l];
int mid = l + (r - l) / 2;
if (nums[mid] > nums[r])
return func(nums, mid + 1, r);
else
return func(nums, l, mid);
}
}
时间复杂度 l o g ( n ) log(n) log(n),空间复杂度为 O ( 1 ) O(1) O(1)
给定一个二叉树,假设边的长度是1。求解二叉树中任意两节点间距离的最大值
示例
输入
5
/ \
3 6
/ \ \
2 4 7
输出:4
对于树的问题,大多数时候都是可以也只能用分治法来解决,这道问题也不例外。由于输入的树的形状不确定,节点间最大距离有三种情况:两棵子树的高度之和、左子树距离最大值、右子树距离最大值,二叉树距离最大值为这三个距离的最大值 m a x D i s t a n c e = M a x ( d e p t h l e f t + d e p t h r i g h t ) , m a x D i s t a n c e l e f t , m a x D i s t a n c e r i g h t maxDistance = Max{(depthleft + depthright), maxDistanceleft , maxDistanceright} maxDistance=Max(depthleft+depthright),maxDistanceleft,maxDistanceright。
class Solution {
public int dis;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return dis;
}
public int maxDepth(TreeNode root) {
if (root == null)
return 0;
int left = maxDepth(root.left);
int right = maxDepth(root.right);
dis = Math.max(dis, left + right);
return Math.max(left, right) + 1;
}
}
需要访问到所有的节点且只访问一次,算法的时间复杂度为 O ( n ) O(n) O(n)
给定一棵完全二叉树,定义若树中的一个点v的value,比它相连的其他节点的value都小,则将这个节点的值作为局部最小值。
示例
输入
5
/ \
3 6
/ \ \
2 4 7
输出:2
这也是一道与树相关的题。很显然需要分治法一层层向下寻找,对于根结点来说,有四种情况:
1.根节点比左右孩子节点都小,则返回根节点;
2.根节点比左孩子小,比右孩子大;
3.根节点比右孩子小,比左孩子大;
4.根节点比左右孩子都大。当我们选择一条value逐渐下降的路径,到达节点v,若v的左右孩子都比其大,则返回v。否则直到找到这样的v,或到达叶子节点,由于是沿着value下降的方向,所以叶子节点是满足条件的。
int TreeNode* FindLocalMin(TreeNode* root){
if(!root->left && !root->right) return root->val;
if(root->left->val > root->val && root->right->val > root->val)
return root->val;
if(root->left->val > root->val && root->right->val < root->val)
return FindLocalMin(root->right);
if(root->left->val < root->val && root->right->Val > root->val)
return FindLocalMin(root->left);
if(root->left->val < root->val && root->right->val < root->val)
return FindLocalMinroot->left;//we can choose either left or right
}
由于每次都舍弃了一个子树,且输入为完全二叉树,因此算法的时间复杂度为 O ( l o g n ) O(log n) O(logn)
求解凸多边形的划分方式
输入:4
输出:2
这也是一道典型的分治法求解的题。假设凸 n n n边形的各个顶点分别为 P 1 , P 2 , . . . , P n P_1,P_2,...,P_n P1,P2,...,Pn,将一条边作为基边,这里假设将 P 1 P n P_1P_n P1Pn作为基边,那么三角形的另外一个顶点必须在 P 2 , . . . , P n P_2,...,P_n P2,...,Pn中选取,假设以 P 1 P n P_1P_n P1Pn为基边的三角形另一个顶点为 P k ( 1 < k < n ) P_k(1
int totalCount(int n){
vector dp(n, 0); //store the result we have computed
//before to reduce the complexity
return helper(dp, n);
}
int helper(vector& dp, int n){
if (n<2) return 0;
if(n==2 || n==3) {
dp[n]=1;
return 1;
}
if(dp[n] != 0) result dp[n];
int result = 0;
for(int i=2;i
如果采用普通的迭代算法,时间复杂度为 O ( n 2 ) O(n^2) O(n2),当存储之前计算过的结果后,时间复杂度降低为 O ( n ) O(n) O(n),但相应的空间复杂度增加至 O ( n ) O(n) O(n)。
给定序列 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an,如果 i < j i
输入:[5,1,2]
输出:1
此题是归并排序的一个变形题,由于归并排序本身就是利用的分治法,因此寻找逆序对的核心思想也是利用分治法。将序列划分为两个子序列,前一半序列中序号均大于后一半的序列,此时前一半序列中的任意值 x i x_i xi与后一半序列中的任意值 x j x_j xj存在关系 x i > 3 ∗ x j x_i>3*x_j xi>3∗xj,那么这就找到了一组逆序对。在比较的同时排序,会使得数组有序,可以进一步降低比较的时间复杂度。
int mergeSort(vector& nums, int left, int right){
int mid = left+(right-left)/2, l = left;
int r = mid, count = 0, flag = left;
while(l (long)3*nums[r]) count += mid - l;
else{
for(;flag (long)3*nums[r]){
count += mid- flag;
break;
}
}
}
r++;
}
}
for(int i=left;i& nums, int left,int right){
if(right-left<2) return 0;
int mid = left+(right-left)/2;
return countInversion(nums, left, mid) + countInversion(nums, mid, right) + mergeSort(nums, left, right);
}
算法的时间复杂度与归并排序的时间复杂度相同 O ( n l o g n ) O(nlogn) O(nlogn),用了一个暂时的数组存储子序列排序完后的序列,空间复杂度为 O ( n ) O(n) O(n)