算法导论 第一部分 第四章-分治策略

算法导论 第一部分 第四章-分治策略

我们知道分治策略,就是3个步骤,分解、解决、合并
子问题足够大,需要递归解决,叫做递归情况
子问题足够小,就进入了基本情况


递归式

递归式可以很方便的表示算法运行时间。比如,我们用以下递归式表示了归并排序的运行时间 T(n);

		{Θ(1)    如果  n = 1;    }
T(n) =  {  				         }
        {2T(n/2)+ Θ(n) 如果 n > 1}

三种解递归式的方法,

  • 代入法,我们猜测一个界,然后用数学归纳法证明是对的
  • 递归树法,把递归式转化为一棵树,节点表示不同层次节点的递归产生的代价,然后用边界和技术求解。
  • 主方法,T(n) = aT(n/b) + f(n), 就是生成a个子问题,每个子问题规模是原问题的1/n,分解和合并花费时间是f(n)

最大子数组问题

买卖股票时机,输入为
[100,113,110,85,105,102,86,63,81,101,94,106,101,79,94,90,97]
索引是第几天的股价。

暴力法求解,取出所有的组合,显然运行时间是 Ω(n2)

分治策略
把源输入,变成每日的价格变化,变为
[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
我们找到数组的中间位置mid, 一个子数组 A[i,j]必有3个情况,
数组的左边部分 A[low, mid]
数组的右边部分 A[mid+1, high]
包含mid点
左右部分的解,可以递归解出。

第三种情况的解法。

function find_max_cross_sub_array(arr, low, mid, high) {
		// 求出左半部分的最大子数组
		let left_sum = -Infinity;
		let sum = 0;
		let max_left = mid;
		for(let i = mid; i >= low; i--)	{
			sum+=arr[i];
			if(sum > left_sum){
				left_sum = sum;
				max_left = i;
			}
		}
		// console.log(`max_left=${max_left} left_sum=${left_sum}`)

		// 求出right半部分的最大子数组
		let right_sum = -Infinity;
		sum = 0;	
		let max_right = mid + 1;
		for(let j = mid+1; j <= high; j++){
			sum+=arr[j];
			if(sum > right_sum){
				right_sum = sum;
				max_right = j;
			}
		}
		// console.log(`max_right=${max_right} right_sum=${right_sum}`)

		// 返回两个边界、两个部分的和 
		return [max_left, max_right, left_sum + right_sum]
	}


合并得到结果

   function find_max_sub_array(arr, low, high) {
    	if(low === high){
    		return [low, high, arr[low]];
    	}
    	let mid = Math.floor((low+high)/2);
    	let [left_low, left_high, left_sum] = find_max_sub_array(arr, low, mid);
    	let [right_low, right_high, right_sum] = find_max_sub_array(arr, mid + 1, high);
    	let [cross_low, cross_high, cross_sum] = find_max_cross_sub_array(arr, low, mid, high);
    	if(left_sum >= right_sum && left_sum >= cross_sum){
    		return [left_low, left_high, left_sum];
    	}else if (right_sum >= left_sum && right_sum >= cross_sum){
    		return [right_low, right_high, right_sum]
    	}else {
    		return [cross_low, cross_high, cross_sum];
    	}
    }

补充 - 更简单的解法

var maxProfit = function(prices) {
  let min = prices[0];
  let max = 0;
  for(let i = 1; i < prices.length; i++) {
    min = Math.min(min, prices[i]);
    max = Math.max(max, prices[i] - min);
  }
  return max;
};

补充 - 连续数组最大的和

var maxSubArray = function(nums) {
    let max = nums[0];
    let maxSum = max;
    for(let i = 1; i < nums.length;i++){
        let c = nums[i];
        max = Math.max(max, c, maxSum + c);
        maxSum = Math.max(c, maxSum + c)
    }
    return max;
};

矩阵

矩阵是矩形的数组

	
A = [ 1 2 3 ]
	[ 4 5 6 ]

是一个 2 * 3的矩阵A = (aij) ,矩阵中的元素为aij
通过交换行和列,得到矩阵A的转置AT

   [ 1 4 ]
	[ 2 5 ]
   [ 3 6 ]

向量是一维数组

   {  2  }
x = {  3  }
	{  5  }

是一个大小是3的向量,长度为n的向量为n向量,
xT = (2,3,5)

矩阵的乘法,就是两个相容的矩阵A和B,就是A的列数和B的行数一样

对于一个 n * n的矩阵乘法,普通的解法如下


  function matrix_multiply(ma, mb) {
    let l = ma.length;
    let r = [];
    for (let x = 0; x < l; x++) {
      r[x] = [];
      for (let y = 0; y < l; y++) {
        let z = 0;
        for (let k = 0; k < l; k++) {
          z += (ma[x][k] + mb[k][y]);
        }
        r[x][y] = z;
      }
    }
    return r;
  }

明显需要花费Θ(n3)时间

使用分治法,分解原矩阵为子矩阵。

  // ma 和 mb都是n的矩阵
  function matrix_multiply_recurse(ma, mb) {
    let n = ma.length;
    let mc = [];
    if (n === 1) {
      mc[1][1] = ma[1][1] * mb[1][1];
    } else {
      //mc11 = matrix_multiply_recurse(ma11, mb11) + matrix_multiply_recurse(ma12, mb21)
      //mc12 = matrix_multiply_recurse(ma11, mb12) + matrix_multiply_recurse(ma12, mb22)
      //mc21 = matrix_multiply_recurse(ma21, mb11) + matrix_multiply_recurse(ma22, mb21)
      //mc22 = matrix_multiply_recurse(ma21, mb12) + matrix_multiply_recurse(ma22, mb22)
    }
    return mc;
  }

使用递归式来表示上述方法,对于n=1,的情况
T(1) = Θ(1)
当n > 1,把一个n问题规模分解为n/2,所以调用一次递归是T(n/2),总共8次, 每个子矩阵有n2/4个元素,每次矩阵加法需要Θ(n2) 的时间
T(n) = 8T(n/2)+ Θ(n2)
最后得到 T(n) = Θ(n3) ,并不优于上述直接的matrix_multiply方法。

以下为具体的实现,略复杂

 // A 和 B 都是n的矩阵,n是2的幂
function f(A, B) {
    let n = A.length;
    let C = [];

    if (n === 1) {
      C[0] = [];
      C[0][0] = A[0][0] * B[0][0];
      return C;
    }

    function add(A, B) {
      let r = [];
      let n = A.length;
      for (let a = 0; a < n; a++) {
        r[a] = [];
        for (let b = 0; b < n; b++) {
          r[a][b] = A[a][b] + B[a][b];
        }
      }
      return r;
    }

    const m = n / 2;
    // 分割两个矩阵
    let A11 = [], A12 = [], A21 = [], A22 = [];
    let B11 = [], B12 = [], B21 = [], B22 = [];

    A.slice(0, m).forEach(l => {
      A11.push(l.slice(0, m));
      A12.push(l.slice(m, n));
    });

    A.slice(m, n).forEach(l => {
      A21.push(l.slice(0, m));
      A22.push(l.slice(m, n));
    });

    B.slice(0, m).forEach(l => {
      B11.push(l.slice(0, m));
      B12.push(l.slice(m, n));
    });

    B.slice(m, n).forEach(l => {
      B21.push(l.slice(0, m));
      B22.push(l.slice(m, n));
    });

    let r1 = add(f(A11, B11), f(A12, B21));
    let r2 = add(f(A11, B12), f(A12, B22));
    let r3 = add(f(A21, B11), f(A22, B21));
    let r4 = add(f(A21, B12), f(A22, B22));
    C = [...r1, ...r3];
    let c2 = [...r2, ...r4];
    for (let i = 0; i < C.length; i++) {
      C[i] = [...C[i], ...c2[i]];
    }
    return C;

  }


strassen方法

就是用常数次矩阵乘法的代价较少了一次矩阵乘法。T(n) = Θ(nlg7)

递归树解递归式

每个节点表示单一子问题的代价,子问题对应某次递归调用。把树中每层的代价求和,就是总的代价。

由 T(n) = 3T(n/4) + cn2;把n替换为n/4得到
得到T(n/4) = 3
T(n/16) + c(n/4)2;

主方法解递归式

主方法就是T(n) = a(n/b) + f(n) 就是把规模是n的子问题划分为a个规模是n/b的子问题,
主定理就是:使得a >= 1和b > 1是常数,f(n)是一个函数T(n)是定义在非负整数上的递归式 T(n) = a(n/b) + f(n)
其中是 n/b 是向上取整或者是向下取整。那么T(n)有以下渐近界。

  1. 如果某个常数 e > 0 有f(n) = Ο(nlogba-e),那么 T(n) = Θ(nlogba);
  2. 如果 f(n) = Θ(nlogba), 则 T(n) = Θ(nlogbalgn);
  3. 如果某个常数 e > 0 有f(n) = Ω(nlogba+e),且对某个常数 c<1 和足够大的n 有af(n/b) <= cf(n) ,则 T(n) = Θ(f(n));

就是我们对比了,f(n)和 nlogba,两个函数较大者决定了递归式的解。如果后者大,就是情况1,如果前者大,就是情况3,如果情况相当,就是情况2.

具体分析,第1种情况,不是 f(n) 小于 nlogba 就够了, 而是多项式意义的小于。也就是说f(n)需要渐近小于 nlogba,要相差一个因子 ne, 其中e是大于0的常数。
第3种情况同样也是。注意,这3种情况并未覆盖f(n)的所有情况,情况1和情况2之间有间隙,2和3之间也有。如果这种情况下,就不可以使用主方法来求递归式。

使用主方法

例子1
T(n) = 9T(n/3) + n
a = 9;
b = 3;
logba = 2;
f(n) = n;
当e=1时,适用第一种情况
那么结果是 T(n) = Θ(n2);

例子2
T(n) = T(2n/3) + 1
a = 1;
b = 1.5;
logba = 0;
f(n) = 1;
适用第二种情况
结果是T(n) = Θ(lgn)

例子3
T(n) = 3T(n/4) + nlgn
a = 3;
b = 4;
logba = log43 = 0.8(约等于)
f(n) = nlgn;
f(n)= n0.8+e,当e=0.2时,可成立情况3
当c = 3/4时,成立。
cf(n) = 3/4 * nlgn
a
f(n/b) = 3* f(n/4) = 3/4 * n lg4/n <= c*f(n) 所以成立

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值