loading

算法设计与分析:分治算法

2-1 众数问题

问题描述

给定含有n个元素的多重集合S,每个元素在S中出现的次数称为该元素的重数。多重集S中重数最大的元素称为众数。例如,S={1,2,2,2,3,5}。多重集S的众数是2,其重数为3。

编程任务:对于给定的由n个自然数组成的多重集S,采用分治算法编程计算S的众数及其重数。

算法描述
  1. 对数组进行排序(例如利用快速排序,时间复杂度为\(O(NlogN)\)
  2. 求出中位数的统计信息
  3. 将数组进行划分,对子数组分别进行递归进行步骤2的求解
关键代码
class Solution {
    int mode;/* 众数 */
    int val;/* 重数 */

    public void solve(int[] a, int l, int r) {
        /* 左闭右闭区间 */
        if (l > r) {
            //区间为空
            return;
        }
        int mid = l + (r - l) / 2;
        int i = mid, j = mid;
        //找到左侧第一个不等于a[mid]的位置
        while (i >= 0 && a[i] == a[mid]) {
            i--;
        }
        //找到右侧第一个不等于a[mid]的位置
        while (a[j] == a[mid] && j <= a.length - 1) {
            j++;
        }
        //j-i-1为中位数的重数
        if (j - i - 1 > val) {
            val = j - i - 1;
            mode = a[mid];
        }
        //左侧可能找到结果
        if (i - l + 1 > val) {
            solve(a, l, i);
        }
        //右侧可能找到结果
        if (r - j + 1 > val) {
            solve(a, j, r);
        }
    }
}
结果分析

空间复杂度\(O(logN)\)

排序的时间复杂度\(O(NlogN)\),单个子问题的时间复杂度\(O(N)\),子问题的个数为\(O(logN)\)

总的时间复杂度为\(O(NlogN)\)

2-4 半数单集问题

问题描述

给定一个自然数n,由n开始可以依次产生半数集set(n)中的数如下:
(1) n ∈set(n);
(2) 在n的左边加上一个自然数,但该自然数不能超过最近添加的数的一半;
(3) 按此规则进行处理,直到不能再添加自然数为止。
注意,半数单集不是多重集,集合中已经有的元素不再添加到集合中。

算法描述

\(f(n)\) = 半数集\(set(n)\)中元素的个数

在这里插入图片描述

由图可知:\(f(6) = 1 + f(1) + f(2) + f(3)\)

归纳得出:

\[f(n) =1 + \sum\limits_{i=1}^{\frac{n}{2}}f(i) \]

并且由此也可以发现,递归过程中存在大量的重叠子问题,可以用记忆化的方式进行优化。

同时题目要求的是半数单集(半数单集不是多重集,集合中已经有的元素不再添加到集合中

在计算中,可能产生重复的元素是两位数

一个两位数\(x\)重复产生的条件是,在1位数\(y=x \% 10\)的半数集中已经产生了\(x\),因此有\(x/10<= y/2\)

关键代码
class Solution {
    int[] memo;

    public Solution() {
        /* 记忆化存储 */
        memo = new int[1000];
    }

    public int halfSingleSet(int n) {
        /* 如果f(n)之前算过,就不必再重复计算 */
        if (memo[n] > 0) {
            return memo[n];
        }
        int sum = 1;
        for (int i = 1; i <= n / 2; i++) {
            sum += halfSingleSet(i);
            /* 重复计算的情况 */
            if (i > 10 && i / 10 <= ((i % 10) / 2)) {
                sum -= halfSingleSet(i / 10);
            }
        }
        memo[n] = sum;
        return sum;
    }
}
结果分析

对于每个\(n\)\(f(n)\)仅会被计算一次,每次计算的时间复杂度是\(O(N)\)

所以总体的时间复杂度是\(O(N^2)\)

2-7 集合划分问题

问题描述

\(n\) 个元素的集合{1,2,…, \(n\) }可以划分为若干个非空子集。

例如,当 \(n\) = 4 时,集合{1,2, 3,4}可以划分为 15 个不同的非空子集如下:
{{1},{2},{3},{4}},
{{1,2},{3},{4}},
{{1,3},{2},{4}},
{{1,4},{2},{3}},
{{2,3},{1},{4}},
{{2,4},{1},{3}},
{{3,4},{1},{2}},
{{1,2},{3,4}},
{{1,3},{2,4}},
{{1,4},{2,3}},
{{1,2,3},{4}},
{{1,2,4},{3}},
{{1,3,4},{2}},
{{2,3,4},{1}},

给定正整数 n ,计算出 n 个元素的集合{1,2,…, n }可以划分为多少个不同的非空子集

算法描述

观察\(n\) = 4非空子集的划分,所有的划分中非空子集的个数为1,2,3, … , n

\(f(n,m)\)表示n个元素的集合{1,2,…,\(n\)}可以划分为多少个不同的由m个非空子集组成的集合

针对于第\(n\)个元素,有两种选择:

  1. \(n\)归入第\(m\)个非空子集
  2. \(n\)单个元素直接作为第\(m\)个非空子集

即有递推关系式:

\[f(n,m) =m*f(n-1,m)+f(n-1,m-1) \]

\[f(n,n)=1 \]

\[f(n,1)=1 \]

而总体的非空子集划分的个数结果为

\[res = \sum\limits_{m=1}^{n}f(n,m) \]

关键代码
class Solution {
    /* n个元素划分为由m个非空子集组成的集合的划分方法数 */
    public int divSet(int n, int m) {
        if (m == n || m == 1) {
            return 1;
        }
        return m * divSet(n - 1, m) + divSet(n - 1, m - 1);
    }
}

结果分析

递归树规模是指数增长的,子问题个数是\(O(2^N)\)的,而每个子问题都在\(O(1)\)时间内完成

所以总的时间复杂度为\(O(2^N)\)

posted @ 2021-11-16 17:09  小羊Ziyan  阅读(320)  评论(0编辑  收藏  举报