算法小课堂(二)递归与分治

目录

一、概念

1.1相关概念

1.2应用场景

1.3递归和迭代的区别

1.4局限性

二、递归相关问题

2.1例题汉诺塔

2.1林大OJ:564汉诺塔-递归

2.1林大OJ:896喜闻乐见汉诺塔

2.2林大OJ:1762fibnacci--递归版

2.2林大OJ:455Fibonacci数列的性质

2.3林大OJ:824函数阶乘

2.3林大OJ:26计算阶乘位数

2.4全排序

2.4林大OJ:1110东林系列故事之字典序与全排列

2.5整数划分问题

三、分治

3.1相关概念

3.2应用场景

3.3常见问题

3.4局限性

四、分治相关问题

4.1二分搜索

4.1林大OJ:956二分查找

4.2归并排序

4.2林大OJ:32枫之舞------排序

4.3快排

4.4sort函数林大OJ:429绝对值排序

4.5循环赛日程表

4.6线性时间选择林大OJ:1684第K小整数-SET


一、概念

1.1相关概念

算法设计与分析是计算机科学的一个重要领域,其目的是设计高效的算法并评估其性能。递归与分治是算法设计与分析中两个基本的概念。

递归是一种解决问题的方法,其中问题被逐步分解为更小的子问题,直到问题的规模变得足够小,可以直接解决。递归算法通常包括一个递归函数,该函数调用自身来解决更小的子问题,直到达到基本情况,然后返回结果。递归算法在许多领域都有应用,如排序、搜索、树和图等数据结构的操作。

分治是一种算法设计策略,其中问题被分成相互独立的子问题,这些子问题分别求解,然后将它们的解合并起来以得到原始问题的解。分治算法通常包括三个步骤:分解、解决和合并。在分解阶段,原始问题被分成若干子问题;在解决阶段,每个子问题独立求解;在合并阶段,将所有子问题的解合并为原始问题的解。分治算法通常用于处理复杂问题,如归并排序、快速排序等排序算法,以及大规模的并行计算等领域。

1.2应用场景

  1. 排序算法:许多排序算法都是通过递归或分治来实现的,如归并排序和快速排序。

  2. 数据结构操作:二叉树、图等数据结构的遍历和操作通常采用递归算法实现。

  3. 搜索算法:许多搜索算法都可以通过递归实现,如深度优先搜索和广度优先搜索。

  4. 图像处理:图像处理中的一些算法,如图像分割和特征提取,可以使用分治算法来加速计算。

  5. 数学计算:递归算法可以用于计算复杂的数学问题,如计算斐波那契数列或计算组合数。

  6. 数据压缩:分治算法可以用于数据压缩中的哈夫曼编码和算术编码。

  7. 并行计算:分治算法可以用于并行计算中,通过将问题分成多个子问题,每个子问题可以在不同的处理器上独立计算。

1.3递归和迭代的区别

递归和迭代都是程序设计中常用的控制流程结构,二者的主要区别在于实现方式和使用场景。

        递归是一种通过函数调用自身来解决问题的方法。递归函数在处理问题时将其拆分为若干个规模较小但结构相同的子问题,然后通过递归调用自身来处理这些子问题。递归函数通常包括两部分:基线条件和递归条件。基线条件指的是问题规模缩小到一定程度后可以直接得出答案的情况,递归条件指的是需要不断缩小问题规模的情况。递归的实现通常简单直观,但可能会导致函数调用栈溢出或运行效率较低等问题。

        迭代是一种通过循环结构来解决问题的方法。迭代函数通过循环控制语句来重复执行某一段程序,直到达到特定条件为止。迭代通常需要使用变量来记录状态信息,以便在下一次循环时更新状态。迭代的实现通常比递归更高效,但可能会导致代码复杂度较高。

        一般来说,递归更适用于问题具有递归结构的情况,例如树形结构、分治算法等。而迭代更适用于问题具有迭代性质的情况,例如循环处理、动态规划等。在实际应用中,需要根据具体情况选择适合的方法来解决问题。

1.4局限性

  1. 递归深度限制:递归算法的缺点之一是可能存在递归深度限制。如果递归深度太大,可能会导致栈溢出或者超时等问题。

  2. 内存开销:分治算法通常需要创建许多临时数组或者其他数据结构来存储中间结果,这会导致额外的内存开销,尤其是在处理大规模数据时。

  3. 子问题之间相互依赖:有些问题可能存在子问题之间相互依赖的情况,这种情况下分治算法无法有效地工作。

  4. 算法复杂度:虽然递归和分治算法通常可以提高算法性能,但是在某些情况下,它们的时间和空间复杂度可能不是最优的。

二、递归相关问题

2.1例题汉诺塔

汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?(每次只能移动1个盘子,大盘子只能放在小盘子下面)

思路分析

具体思路如下:

  1. 将圆盘从起始柱子移动到目标柱子需要借助空闲柱子,因此需要找到空闲柱子。
  2. 将起始柱子上的n-1个圆盘移动到空闲柱子上,此时起始柱子上只剩下最大的圆盘。
  3. 将起始柱子上的最大圆盘移动到目标柱子上。
  4. 将空闲柱子上的n-1个圆盘移动到目标柱子上,此时所有圆盘都在目标柱子上。

递归算法的具体步骤如下:

  1. 判断当前要移动的圆盘数量,如果只有一个圆盘,则直接移动到目标柱子。
  2. 如果圆盘数量大于1,则先将n-1个圆盘从起始柱子移动到空闲柱子,再将最大的圆盘从起始柱子移动到目标柱子,最后将n-1个圆盘从空闲柱子移动到目标柱子。
#include <iostream>
using namespace std;

void hanoi(int n, char start, char end, char temp) {
    if (n == 1) {
        cout << "Move disk 1 from rod " << start << " to rod " << end << endl;
        return;
    }
    hanoi(n - 1, start, temp, end);
    cout << "Move disk " << n << " from rod " << start << " to rod " << end << endl;
    hanoi(n - 1, temp, end, start);
}

int main() {
    int n;
    while(cin>>n&&n){
        hanoi(n, 'A', 'C', 'B');
    }
    return 0;
}

2.1林大OJ:564汉诺塔-递归

Description

从前有一座庙,庙里有三个柱子,柱A柱 B柱 C。柱A有64个盘子,从上往下盘子越来越大。要求庙里的老和尚把这64个盘子全部移动到柱子C上。移动的时候始终只能小盘子压着大盘子。而且每次只能移动一个。
现在问题来了,老和尚相知道将柱A上面前n个盘子从柱A搬到柱C搬动方法。要求移动次数最少。

Input

输入有多组,每组输入一个正整数n(0<n<16)

Output

每组测试实例,输出每一步的步骤,输出“number..a..form..b..to..c”。表示将第a个盘子从柱b搬到柱c.

Sample Input

2

Sample Output

number..1..form..A..to..B
number..2..form..A..to..C
number..1..form..B..to..C
#include <bits/stdc++.h>
using namespace std;


void movement(int num, char a, char b) {
    printf("number..%d..form..%c..to..%c\n", num, a, b);
}

void hanoi(int n, char A, char B, char C) {
    if (n == 1) {
        movement(1, A, C);
    } else {
        hanoi(n-1, A, C, B);
        movement(n, A, C);
        hanoi(n-1, B, A, C);
    }
}

int main() {
    int n;
    while (~scanf("%d",&n)) {
        hanoi(n, 'A', 'B', 'C');
    }
    return 0;
}

2.1林大OJ:896喜闻乐见汉诺塔

Description

现在有三根柱子(分别在左边,中间,右边),最左边柱子上有n个圆盘,从小到大依次摞着,现在我想把这n个圆盘移动到最右边的柱子上。 移动必须满足如下要求: (1):每个圆盘不能直接从最左边移到最右边(必须经过中间的杆子),同时也不能直接从最右边移到最左边。 (2):在移动的过程中必须保证小盘在大盘上面

Input

输入数据由多行组成,每行包含一个整数n(1<=n<=35)。

Output

对于每个测试样例,输出将n个盘移到最右边的最少次数。

Sample Input

1
3
12

Sample Output

2
26
531440

Hint

用递推的思想,先考虑规模为N-1的情况,再考虑由N-1扩展到N。

公式法

#include <iostream>
#include <cmath>

using namespace std;

// 求解移动最小的次数
long long hanoi(int n)
{
    if (n == 1)
        return 2;  // 只有一个盘子时需要移动2次
    return 3 * hanoi(n - 1) + 2 ; // 其他情况根据递推公式求解
}

int main()
{
    int n;
    while (cin >> n)
    {
        cout << hanoi(n) << endl;
    }
    return 0;
}

2.2林大OJ:1762fibnacci--递归版

Description

 已知数列 1   1    2      3     5     8    13  

Input

计算第n项的值  (1<=n<=15)

Output

输出题意

Sample Input

3

Sample Output

2
#include <iostream>
using namespace std;

int fibonacci(int n) {
    if (n <= 2) {
        return 1;
    }
    return fibonacci(n-1) + fibonacci(n-2);
}

int main() {
    int n;
    cin >> n;
    cout << fibonacci(n) << endl;
    return 0;
}

矩阵快速幂模板

矩阵快速幂是一种高效求解斐波那契数列的方法,可以将时间复杂度降到O(log n)级别。

#include <iostream>
#include <vector>
using namespace std;

// 定义矩阵类型
typedef vector<vector<long long>> matrix;

// 矩阵乘法
matrix multiply(const matrix& A, const matrix& B) {
    int n = A.size(), m = A[0].size(), k = B[0].size();
    matrix C(n, vector<long long>(k));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < k; j++) {
            for (int p = 0; p < m; p++) {
                C[i][j] += A[i][p] * B[p][j];
            }
        }
    }
    return C;
}

// 矩阵快速幂
matrix matrix_pow(matrix A, int n) {
    matrix ans(A.size(), vector<long long>(A.size()));
    // 初始化为单位矩阵
    for (int i = 0; i < ans.size(); i++) {
        ans[i][i] = 1;
    }
    while (n > 0) {
        if (n & 1) {
            ans = multiply(ans, A);
        }
        A = multiply(A, A);
        n >>= 1;
    }
    return ans;
}

// 计算Fibonacci数列的第n项
long long fibonacci(int n) {
    if (n <= 0) {
        return 0;
    }
    matrix A = {{1, 1}, {1, 0}};  // 初始矩阵
    matrix ans = matrix_pow(A, n-1);
    return ans[0][0];
}

int main() {
    int n;
    while(cin >> n){
        cout << fibonacci(n) << endl;
    }
    return 0;
}

动态规划

#include <iostream>
using namespace std;

int main() {
    int n;
    long long fib[51]; // 数组用于存储已知数列
    while(~scanf("%d",&n)){
        fib[1] = fib[2] = 1; // 已知数列的前两项
        for (int i = 3; i <= n; i++) {
            fib[i] = fib[i-1] + fib[i-2]; // 动态规划递推式
        }
        cout << fib[n] << endl; // 输出第n项的值
    }

    return 0;
}

2.2林大OJ:455Fibonacci数列的性质

Description

Fibonacci数列定义如下: a[1]=1 a[2]=1 a[n]=a[n-1]+a[n-2] n>2 对于给定N (1 ≤ N ≤10000),请判别数列第N项的奇偶性。

Input

给定整数N,如N=0则结束输入(N=0不需要判断)。

Output

输出第N项Fibonacci数列的奇偶性,如果为奇数则请输出“ODD”,否则为“EVEN”。 

Sample Input

1
2
3
0

Sample Output

ODD
ODD
EVEN

Hint

找规律或奇数+奇数=偶,偶+奇=奇数
#include <iostream>
using namespace std;

int main() {
    int n;
    while (cin >> n && n != 0) {
        int a = 1, b = 1, c;
        for (int i = 3; i <= n; i++) {
            c = (a + b) % 2;
            a = b % 1000000007;
            b = c % 1000000007;
        }
        if (n == 1 || n == 2) {
            cout << "ODD" << endl;
        } else if (c == 0) {
            cout << "EVEN" << endl;
        } else {
            cout << "ODD" << endl;
        }
    }
    return 0;
}

2.2林大OJ:95数字取余

Description

已知数列:f(1)=1,f(2)=2,f(n)=(4*f(n-1)+7*f(n-2))%11,请你计算f(n)的值!

Input

有多组数据,每组数据占1行,每行1个数n,(1<=n<=1500000000)

Output

输出f(n)的值.

Sample Input

1
2
3

Sample Output

1
2
4
#include <iostream>
#include <cstring>
using namespace std;

const int MOD = 11;

// 矩阵乘法
void matrix_multiply(int a[][2], int b[][2])
{
    int c[2][2];
    memset(c, 0, sizeof(c));
    for (int i = 0; i < 2; i++)
        for (int j = 0; j < 2; j++)
            for (int k = 0; k < 2; k++)
                c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % MOD;
    memcpy(a, c, sizeof(c));
}

// 矩阵快速幂
void matrix_pow(int a[][2], int n)
{
    int res[2][2];
    memset(res, 0, sizeof(res));
    res[0][0] = res[1][1] = 1;
    while (n)
    {
        if (n & 1) matrix_multiply(res, a);
        matrix_multiply(a, a);
        n >>= 1;
    }
    memcpy(a, res, sizeof(res));
}

// 计算f(n)
int solve(int n)
{
    if (n == 1) return 1;
    if (n == 2) return 2;
    int a[2][2] = {{4, 7}, {1, 0}};
    matrix_pow(a, n - 2);
    return (2 * a[0][0] + a[0][1]) % MOD;
}

int main()
{
    int n;
    while (cin >> n)
        cout << solve(n) << endl;
    return 0;
}

2.3林大OJ:824函数阶乘

Description

从键盘输入一个数值n(n <= 15),计算出数值n的阶乘。

Input

输入数据有多组,每组1个整数值n (0 <= n <=15)。

Output

对应输入数值的阶乘的值。

Sample Input

0
1
2
3
10

Sample Output

1
1 
2
6
3628800
#include <iostream>
using namespace std;

long long  factorial(int n) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        return n * factorial(n-1);
    }
}

int main() {
    int n;
    while (cin >> n) {
        long long  result = factorial(n);
        cout << result << endl;
    }
    return 0;
}

进阶版采用高精度乘法的思想

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;

const int MAXN = 10010;
int n, ans[MAXN];

int main() {
    while (scanf("%d", &n) != EOF) {
        memset(ans, 0, sizeof(ans));
        ans[0] = ans[1] = 1; // 初始化答案为1
        for (int i = 2; i <= n; i++) { // 从2开始循环,依次计算n的阶乘
            int carry = 0; // 进位
            for (int j = 1; j <= ans[0]; j++) { // 从低位到高位依次计算
                ans[j] = ans[j] * i + carry; // 乘以i并加上进位
                carry = ans[j] / 10; // 计算进位
                ans[j] %= 10; // 取个位
            }
            while (carry) { // 处理最高位的进位
                ans[++ans[0]] = carry % 10;
                carry /= 10;
            }
        }
        for (int i = ans[0]; i >= 1; i--) { // 逆序输出答案
            printf("%d", ans[i]);
        }
        printf("\n");
    }
    return 0;
}

思路:采用高精度乘法的思想,从2开始循环,依次计算n的阶乘。在每一次乘法中,先计算出进位,然后将结果取个位,最后再处理最高位的进位。最后逆序输出答案即可。由于n的范围较大,因此需要用高精度实现。

2.3林大OJ:26计算阶乘位数

Description

根据密码学需要,要计算某些数的阶乘的位数.

Input

第一行为整数n ,接下来 n 行, 每行1个数m (1 ≤ m ≤ 10^7) . 

Output

输出m的阶乘的位数.

Sample Input

2

10

20

Sample Output

7

19

Hint

Source

#include <iostream>
#include <cmath>
using namespace std;

int main() {
    int t; // 测试数据组数
    cin >> t;
    while (t--) { // 执行t次
        int n; // 输入的正整数
        cin >> n;
        // 计算阶乘n!的位数
        int digits = static_cast<int>((log10(2 * M_PI * n) * 0.5 + n * log10(n / exp(1))) + 1);
        cout << digits << endl; // 输出阶乘n!的位数
    }
    return 0;
}

2.3林大OJ:

Description

从输入中读取一个数n,求出n!中末尾0的个数。

Input

输入有若干行。第一行上有一个整数m,指明接下来的数字的个数。然后是m行,每一行包含一个确定的正整数n,1<=n<=1000000000。

Output

对输入行中的每一个数据n,输出一行,其内容是n!中末尾0的个数。

Sample Input

3
3
100
1024

Sample Output

0
24
253
#include <iostream>
using namespace std;

int main() {
    int m;
    cin >> m;  // 读入询问的数量
    while (m--) {  // 循环处理每个询问
        int n;
        cin >> n;  // 读入当前询问的值
        int cnt = 0;
        while (n >= 5) {  // 不断将 n 除以 5,累加因子个数
            cnt += n / 5;
            n /= 5;
        }
        cout << cnt << endl;  // 输出末尾 0 的个数
    }
    return 0;
}

代码的思路很简单,就是不断将 n 除以 5,并累加因子个数,直到 n小于 5 为止,最终得到的累加值就是 n!末尾 0$的个数。

这个方法的原理是,一个数 n 的末尾 0的个数,等于它的质因子分解中 5 的个数。因为每出现一个 5,就可以和一个 2配对,贡献一个 10,即一个末尾 0。注意,2的个数一定比 5 多,因此只需要计算 5的个数即可。

2.4全排序

【例】排列问题。设R={r1,r2,···,rn}是要进行排列的n个元素,R=R-{r1}。集合X中元素的全排列记为Perm(X)。(ri)Perm(X)表示在全排列 Pe的每个排列前加上前缀得到的排列。R的全排列可归纳定义如下:
当n=1时,Perm(R)=(r),其中r是集合R中唯一的元素;
n>1时,PerPerm(R)由(r1)Perm(R1),(r2)Perm(R2),···,(rn)Perm(Rn)构成。

依此递归定义,可设计产生Perm(R)的递归算法如下:

#include <iostream>
#include <algorithm>

using namespace std;

void perm(int R[], int k, int m) {
    if (k == m) { // 当前排列已经填满
        for (int i = 0; i <= m; i++) {
            cout << R[i] << " ";
        }
        cout << endl;
        return;
    }
    for (int i = k; i <= m; i++) {
        swap(R[k], R[i]); // 将当前位置的元素与后面的元素交换
        perm(R, k + 1, m); // 递归填写下一个位置
        swap(R[k], R[i]); // 还原当前位置的元素
    }
}

int main() {
    int n;
    cin >> n; // 输入元素个数
    int R[n];
    for (int i = 0; i < n; i++) {
        cin >> R[i]; // 输入要进行排列的元素
    }
    sort(R, R + n); // 对元素进行排序
    perm(R, 0, n - 1); // 对整个数组进行排列
    return 0;
}

其中,k表示当前正在填写第k个位置,m表示一共需要填写m个位置。当k==m时,意味着当前排列已经填满,可以输出并返回。否则,从第k个位置开始,枚举后面所有元素与当前位置的元素交换,然后递归填写下一个位置,最后还原当前位置的元素,以便对下一个元素进行处理。

在主函数中,先读入元素个数n和元素数组R,然后对R进行排序,最后调用perm函数生成全排列。

2.4林大OJ:1110东林系列故事之字典序与全排列

Description

    从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。对于数字1、2、3......n的排列,不同排列的先后关系是从左到右逐个比较对应的数字的先后来决定的。例如对于5个数字的排列 1 2 3 5 4和1 2 3 4 5,排列1 2 3 4 5在前,排列1 2 3 5 4在后。按照这样的规定,5个数字的所有的排列中最前面(字典序最小)的是1 2 3 4 5,最后面(字典序最大)的是 5 4 3 2 1。现在数学老师随便给出一个序列,你能帮林林算出这个序列字典序最小和字典序最大的排列是什么吗?

Input

输入有多组,每组有两行。第一行为一个数字n(1<=n<=100),第二行为n个数a0~an-1,其中ai为int范围内的数

Output

输出有两行。第一行”min:”开头,然后是字典序最小的排列,第二行”max:”开头,然后是字典序最大的排列。每行末尾无多余的空格

Sample Input

1
1 2 3 4 5

Sample Output

min: 1
max: 1
min: 1 2 3 4 5
max: 5 4 3 2 1

递归

#include <iostream>
#include <algorithm>

using namespace std;

void perm(int R[], int k, int m, int &minval, int &maxval) {
    if (k == m) { // 当前排列已经填满
        int val = 0;
        for (int i = 0; i <= m; i++) {
            val = val * 10 + R[i]; // 将当前排列转换为一个整数
        }
        if (val < minval) { // 更新字典序最小的排列
            minval = val;
        }
        if (val > maxval) { // 更新字典序最大的排列
            maxval = val;
        }
        return;
    }
    for (int i = k; i <= m; i++) {
        swap(R[k], R[i]); // 将当前位置的元素与后面的元素交换
        perm(R, k + 1, m, minval, maxval); // 递归填写下一个位置
        swap(R[k], R[i]); // 还原当前位置的元素
    }
}

int main() {
    int n;
    while (cin >> n) { // 处理多组输入
        int R[n];
        for (int i = 0; i < n; i++) {
            cin >> R[i]; // 输入要进行排列的元素
        }
        sort(R, R + n); // 对元素进行排序
        int minval = 0x7fffffff, maxval = 0;
        perm(R, 0, n - 1, minval, maxval); // 对整个数组进行排列
        cout << "min: " << minval << endl; // 输出字典序最小的排列
        cout << "max: " << maxval << endl; // 输出字典序最大的排列
    }
    return 0;
}

迭代

#include <iostream>
#include <algorithm>

using namespace std;

void perm(int R[], int n, int &minval, int &maxval) {
    sort(R, R + n); // 对元素进行排序
    int val = 0;
    for (int i = 0; i < n; i++) {
        val = val * 10 + R[i]; // 将当前排列转换为一个整数
    }
    minval = val;
    maxval = val;
    while (next_permutation(R, R + n)) { // 生成所有的排列
        val = 0;
        for (int i = 0; i < n; i++) {
            val = val * 10 + R[i]; // 将当前排列转换为一个整数
        }
        if (val < minval) { // 更新字典序最小的排列
            minval = val;
        }
        if (val > maxval) { // 更新字典序最大的排列
            maxval = val;
        }
    }
}

int main() {
    int n;
    while (cin >> n) { // 处理多组输入
        int R[n];
        for (int i = 0; i < n; i++) {
            cin >> R[i]; // 输入要进行排列的元素
        }
        int minval, maxval;
        perm(R, n, minval, maxval); // 对整个数组进行排列
        cout << "min: " << minval << endl; // 输出字典序最小的排列
        cout << "max: " << maxval << endl; // 输出字典序最大的排列
    }
    return 0;
}

2.5整数划分问题

整数划分问题是将一个正整数n分解成若干个正整数的和,其中每个正整数都可以重复使用,且分解出来的数的个数是不限定的。例如,将整数4分解成若干个正整数的和,可以有以下五种不同的分解方式:

  • 4
  • 3 + 1
  • 2 + 2
  • 2 + 1 + 1
  • 1 + 1 + 1 + 1

经典的整数划分问题是要求对于给定的正整数n,计算将n分解成若干个正整数的和的所有不同的分解方式的数量,也就是求整数n的划分数。

递归

#include <iostream>
using namespace std;

const int MAXN = 100; // 数组大小

void partition(int n, int max_num, int nums[], int len) {
    if (n == 0) {
        // 找到一种划分方案,输出
        for (int i = 0; i < len-1; i++) {
            cout << nums[i] << "+";
        }
        cout << nums[len-1] << endl;
    }
    else if (n > 0 && max_num > 0) {
        // 枚举可以选的数
        for (int i = max_num; i >= 1; i--) {
            nums[len] = i;
            partition(n-i, i, nums, len+1);
        }
    }
}

int main() {
    int n;
    while(~scanf("%d",&n)){
        int nums[MAXN]; // 数组
        partition(n, n, nums, 0); // 调用函数
    }
    return 0;
}

三、分治

3.1相关概念

分治算法是一种常见的算法设计技巧,它把问题划分成多个子问题,然后递归地解决每个子问题,最终将子问题的解合并成原问题的解。

具体来说,分治算法包含三个步骤:

  1. 分解:将原问题划分成若干个规模更小的子问题。

  2. 解决:递归地求解每个子问题。

  3. 合并:将子问题的解合并成原问题的解。

3.2应用场景

分治算法通常适用于以下类型的问题:

  1. 问题可以划分成若干个规模更小的子问题。

  2. 子问题的解可以合并成原问题的解。

  3. 子问题可以独立地求解,即子问题之间没有相关性。

  4. 分治算法的时间复杂度为O(nlogn),优于朴素算法的O(n^2)

3.3常见问题

常见的应用分治算法的问题包括:

  1. 归并排序

  2. 快速排序

  3. 棋盘覆盖

  4. 汉诺塔

  5. 最大子数组问题

  6. 矩阵乘法

3.4局限性

  1. 子问题之间的相关性:分治算法要求子问题之间是独立的,如果子问题之间存在相关性,分治算法就不能使用。在这种情况下,需要考虑其他算法设计技巧。

  2. 额外空间的使用:分治算法需要额外的递归栈空间,可能会增加程序的空间复杂度。

  3. 分治的过程可能不止一次:有些问题需要多次分治才能求解,这会增加算法的复杂度。例如,在矩阵乘法中,需要将矩阵划分成多个子矩阵,然后再对子矩阵进行矩阵乘法。

  4. 分治划分的子问题可能不均衡:在某些情况下,分治算法可能会出现子问题的规模不均衡,这会导致算法的效率降低。例如,在快速排序中,如果选择的主元素不好,就可能会导致分治后的子问题规模不均衡。

四、分治相关问题

4.1二分搜索

#include <iostream>

using namespace std;


int binary_search(int arr[], int left, int right, int target) {
    if (left > right) {
        return -1;  // 没有找到目标元素,返回 -1
    }

    int mid = (left + right) / 2;
    if (arr[mid] == target) {
        return mid;
    } else if (arr[mid] < target) {
        return binary_search(arr, mid + 1, right, target);
    } else {
        return binary_search(arr, left, mid - 1, target);
    }
}

int main() {
    int arr[] = {1, 3, 5, 7, 9, 11};
    int n = sizeof(arr) / sizeof(arr[0]);

    int target = 5;
    int idx = binary_search(arr, 0, n - 1, target);
    if (idx == -1) {
        cout << "没有找到目标元素" << endl;
    } else {
        cout << "目标元素的下标为:" << idx << endl;
    }

    target = 8;
    idx = binary_search(arr, 0, n - 1, target);
    if (idx == -1) {
        cout << "没有找到目标元素" << endl;
    } else {
        cout << "目标元素的下标为:" << idx << endl;
    }

    return 0;
}

4.1林大OJ:956二分查找

Description

有n(1<=n<=2000005)个整数,是乱序的,现在另外给一个整数x,请找出序列排序后的第1个大于x的数的下标!

Input

输入数据包含多个测试实例,每组数据由两行组成,第一行是n和x,第二行是已经有序的n个整数的数列。

Output

对于每个测试实例,请找出序列中第1个大于x的数的下标!。

Sample Input

3 3
1 4 2

Sample Output

2
#include <iostream>
#include <algorithm>
using namespace std;

template <typename T>
int binary_search(T arr[], int left, int right, T target) {
    if (left > right) {
        return left;  // 返回第一个大于目标元素的元素的下标
    }

    int mid = (left + right) / 2;
    if (arr[mid] <= target) {
        return binary_search(arr, mid + 1, right, target);
    } else {
        return binary_search(arr, left, mid - 1, target);
    }
}

int main() {
    int n, x;
    while (scanf("%d%d", &n, &x) != -1) {
        int arr[n];
        for (int i = 0; i < n; i++) {
            scanf("%d", &arr[i]);
        }
        sort(arr, arr + n);
        int idx = binary_search(arr, 0, n - 1, x);
        if (idx >= n) {
            cout << "没有大于目标元素的元素" << endl;
        } else {
            cout << idx << endl;
        }
    }

    return 0;
}

4.2归并排序

#include <iostream>

using namespace std;

const int N = 1e5 + 10;  // 数组的最大长度
int arr[N], tmp[N];     // 数组和临时数组

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

int main() {
    int n;
    cin >> n;

    for (int i = 0; i < n; ++i) {
        cin >> arr[i];
    }

    merge_sort(arr, 0, n - 1);

    for (int i = 0; i < n; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}

4.2林大OJ:32枫之舞------排序

Description

你会排序吗?请将给出的数据按(升序)排序~~ 

Input

输入数据有多组,首先输入一个T,代表有T组数,然后是T行,每行开头是N,代表有N个数需要排序,然后就是这N个要排序的数了!N大于1且小于1000. 

Output

把数据从小到大排序。

Sample Input

2
3 2 1 3
9 1 4 7 2 5 8 3 6 9

Sample Output

1 2 3
1 2 3 4 5 6 7 8 9
#include <iostream>

using namespace std;

const int N = 1e5 + 10;  // 数组的最大长度
int arr[N], tmp[N];     // 数组和临时数组

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

int main() {
    int n;
    while(cin >> n){
    int m;
    while(n--){
        cin>>m;
        for (int i = 0; i < m; ++i) {
            cin >> arr[i];
        }
        merge_sort(arr, 0, m - 1);

        for (int i = 0; i < m - 1; ++i) {
            cout << arr[i] << " ";
            }
        cout <<arr[m-1]<< endl;
        }
    }
    return 0;
}

4.2林大OJ:1337数组的逆序

Description

对于一个数组,如果其中一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。现在给你一个数组,需要你求出该数组的逆序数。

Input

输入有多组数据,每组数据的第一行是一个整数n(1<=n<=10^5),第二行是n个整数a1,a2...,an,(0<=an<=10^5),输入以EOF结束。

Output

输出该数组的逆序数。

Sample Input

10
1 2 3 4 5 6 7 8 9 10
10
10 9 8 7 6 5 4 3 2 1

Sample Output

0
45
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100010;
int arr[N];
int nums;
unsigned long result = 0;

void merge_sort(int arr[], int l, int r)
{
    if (l >= r) return;
    int mid = ( l + r  ) / 2;
    merge_sort(arr, l, mid);
    merge_sort(arr, mid + 1, r);
    int temp[r - l + 1];
    int lptr = l;
    int rptr = mid + 1;
    int tempptr = 0;
    while(lptr <= mid && rptr <= r)
    {
        if(arr[lptr] <= arr[rptr])
        {
            temp[tempptr++] = arr[lptr++];
        } else {
            temp[tempptr++] = arr[rptr++];
            result += (mid - lptr + 1);
        }
    }
    while ( lptr <= mid )
    {
        temp[tempptr++] = arr[lptr++];
    }
    while ( rptr <= r )
    {
        temp[tempptr++] = arr[rptr++];
    }
    for (int i = l, j = 0; i <= r; i ++, j ++)
    {
        arr[i] = temp[j];
    }
}

int main(){
    while(cin >> nums){
        for(int i = 0; i < nums; i++){
            cin >> arr[i];
        }
        merge_sort(arr, 0, nums-1);
        cout << result << endl;
        result = 0; // 清零
    }
    return 0;
}

4.3快排

#include<algorithm>
#include<cstdio>

using namespace std;

const int N=1e5+5;
int q[N];
void quick_sort(int *q,int l,int r)
{
    if(l>=r)return;
    int i=l-1,j=r+1,x=q[l+r+1>>1];
    while(i<j)
    {
        do i++;while(q[i]<x);
        do j--;while(q[j]>x);
        if(i<j)swap(q[i],q[j]);
    }
    quick_sort(q,l,i - 1),quick_sort(q,i,r);
}

int main()
{
    int n=0,x;
    while(~scanf("%d",&x)&&x)q[++n]=x;
    quick_sort(q,1,n);
    for(int i=1;i<=n;i++)printf("%d ",q[i]);
}

4.4sort函数林大OJ:429绝对值排序

Description

输入n(n<=100)个整数,按照绝对值从大到小排序后输出。题目保证对于每一个测试实例,所有的数的绝对值都不相等。

Input

输入数据有多组,每组占一行,每行的第一个数字为n,接着是n个整数,n=0表示输入数据的结束,不做处理。 

Output

对于每个测试实例,输出排序后的结果,两个数之间用一个空格隔开。每个测试实例占一行。

Sample Input

3 3 -4 2
4 0 1 2 -3
0

Sample Output

-4 3 2
-3 2 1 0
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

bool cmp(int a, int b) {
    return abs(a) > abs(b);  // 按绝对值从大到小排序
}

int main() {
    int n;
    while (cin >> n && n != 0) {
        int nums[n];
        for (int i = 0; i < n; i++) {
            cin >> nums[i];
        }
        sort(nums, nums + n, cmp);  // 排序
        for (int i = 0; i < n-1; i++) {
            cout << nums[i] << " ";
        }
        cout << nums[n-1]<< endl;
    }
    return 0;
}

4.5循环赛日程表

题目描述:

有n必须是2的整数次幂个选手参加网球比赛,设计一种日程表,满足以下条件:

1.每个选手必须与其他n-1个选手各赛一次。

2.每个选手-天只能赛一次。

3.循环赛共进行n-1天。

思路:

根据题意,我们可以想到用递归的方法来解决这个问题。将n个选手分成两半,前一半和后一半,分别为A组和B组,进行比赛。这样,原问题就被分成了两个子问题,为A组和B组各自的日程表。我们将两个子问题分别递归解决,直到只有两个选手时,只需要让他们比赛即可。

对于n=2时,两个选手进行一次比赛即可,日程表为(1 vs 2)。

对于n>2时,我们将选手分成A组和B组,A组中的第i个选手和B组中的第i个选手比赛,A组中的第i+j个选手和B组中的第i+j+1(mod n/2)个选手比赛,直到A组中的第i+n/2-1个选手和B组中的第i+n/2-1个选手比赛,其中i为轮次,j为每个选手在每个轮次中比赛的次数。

具体实现可以使用递归的方式,不断将选手分成A组和B组,直到只剩下两个选手,然后逐层合并日程表。

#include <iostream>
using namespace std;

// 输出日程表
void printSchedule(int n, int **schedule) {
    cout << "日程表:" << endl;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            cout << schedule[i][j] << " ";
        }
        cout << endl;
    }
}

// 生成循环赛日程表
void generateSchedule(int n, int **schedule) {
    // n=2时直接生成日程表
    if(n == 2) {
        schedule[1][1] = 1;
        schedule[1][2] = 2;
        schedule[2][1] = 2;
        schedule[2][2] = 1;
    } else {
        // 将选手分为A组和B组
        int **scheduleA = new int*[n/2+1];
        int **scheduleB = new int*[n/2+1];
        for(int i = 0; i <= n/2; i++) {
            scheduleA[i] = new int[n/2+1];
            scheduleB[i] = new int[n/2+1];
        }
        generateSchedule(n/2, scheduleA);
        generateSchedule(n/2, scheduleB);

        // 合并日程表
        for(int i = 1; i <= n/2; i++) {
            for(int j = 1; j <= n/2; j++) {
                schedule[i][j] = scheduleA[i][j];
                schedule[i][j+n/2] = scheduleB[i][j] + n/2;
                schedule[i+n/2][j] = scheduleB[i][j] + n/2;
                schedule[i+n/2][j+n/2] = scheduleA[i][j];
            }
        }
    }
}

int main() {
    int n;
    cout << "请输入选手人数(必须是2的整数次幂):";
    cin >> n;

    // 初始化日程表
    int **schedule = new int*[n+1];
    for(int i = 0; i <= n; i++) {
        schedule[i] = new int[n+1];
    }
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            schedule[i][j] = 0;
        }
    }

    generateSchedule(n, schedule);
    printSchedule(n, schedule);

    // 释放内存
    for(int i = 0; i <= n; i++) {
        delete[] schedule[i];
    }
    delete[] schedule;

    return 0;
}

这段代码实现了一个循环赛日程表生成器,主要思想是使用分治算法将选手分为两个组,递归生成每个组的日程表,最后将两个组的日程表合并成一个完整的日程表。

首先,用户输入选手人数n,然后初始化一个n+1行n+1列的二维数组schedule,并将其全部赋值为0。

接下来是generateSchedule函数,该函数接受两个参数:选手人数n和二维数组schedule,用于生成n个选手的循环赛日程表。

如果选手人数n等于2,则直接生成日程表,将schedule数组赋值为:

schedule[1][1] = 1; schedule[1][2] = 2; schedule[2][1] = 2; schedule[2][2] = 1;

否则,将选手分为A组和B组,然后递归调用generateSchedule函数生成A组和B组的日程表。

接着,合并A组和B组的日程表,具体方法是:将A组的日程表复制到schedule数组的左上角,将B组的日程表复制到schedule数组的右下角,并将B组的日程表中的选手编号加上n/2。这样,就得到了n个选手的完整日程表。

最后,printSchedule函数用于输出日程表,它接受两个参数:选手人数n和二维数组schedule。它会按行输出日程表中的选手编号,用于查看生成的日程表是否正确。

合并过程

这段代码实现了合并两个日程表的过程,其中scheduleAscheduleB分别是两个大小为n/2的子问题的解。

在合并过程中,我们将选手分成两组,分别为A组和B组,然后将A组和B组的日程表合并成一个大的日程表。这个大的日程表的行和列都是原来的两个子问题的两倍。

具体来说,首先我们使用两个嵌套循环遍历scheduleAscheduleB中的所有元素。对于每个元素,我们将其分别复制到大的日程表的四个角落中,如下所示:

  1. schedule[i][j] = scheduleA[i][j]; 复制到左上角
  2. schedule[i][j+n/2] = scheduleB[i][j] + n/2; 复制到右上角
  3. schedule[i+n/2][j] = scheduleB[i][j] + n/2; 复制到左下角
  4. schedule[i+n/2][j+n/2] = scheduleA[i][j]; 复制到右下角

其中,第2、3个复制操作中的+ n/2是为了保证B组中的选手编号正确,因为在大的日程表中,B组的选手编号是从n/2 + 1开始的。

在这段代码中,变量 n 表示当前循环赛的参赛选手人数。因为每次递归调用 generateSchedule 函数时,都会将参赛选手数量减半,所以最终参赛选手人数可能会变为 2,此时就不需要再进行递归调用,而是直接生成日程表。

当 n 为偶数时,每个选手都可以分为两个组,分别为 A 组和 B 组。为了构建完整的日程表,需要在 A 组和 B 组之间安排比赛。而因为 A 组和 B 组的选手数量相等,所以可以将 B 组中的选手编号都加上 n/2,来与 A 组的选手编号区分开来。

在合并日程表的过程中,对于每一个 A 组和 B 组中的选手,都会与另一个组中同样位置的选手进行比赛。因此,可以用两个循环嵌套来遍历所有的选手,同时对于每一个选手,在四个位置上填上比赛对手的编号:

  • schedule[i][j] 对应 A 组中第 i 个选手和 B 组中第 j 个选手的比赛
  • schedule[i][j+n/2] 对应 A 组中第 i 个选手和 B 组中第 j 个选手+n/2 的比赛
  • schedule[i+n/2][j] 对应 B 组中第 i 个选手和 A 组中第 j 个选手+n/2 的比赛
  • schedule[i+n/2][j+n/2] 对应 B 组中第 i 个选手+n/2 和 A 组中第 j 个选手的比赛

通过这种方式,可以确保所有的选手都与其他选手进行了一次比赛,且没有重复的比赛。最终的日程表就可以通过 printSchedule 函数来输出。

4.6线性时间选择林大OJ:1684第K小整数-SET

Description

现有n个正整数,n≤10000,要求出这n个正整数中的第k个最小整数(相同大小的整数只计算一次),k≤1000,正整数均小于30000。

Input

第一行为n和k; 第二行开始为n个正整数的值,整数间用空格隔开。

Output

第k个最小整数的值;若无解,则输出“NO RESULT”。

Sample Input

10 3
1 3 3 7 2 5 1 2 4 6

Sample Output

3
#include <iostream>
#include <vector>

using namespace std;
vector<int> a;


int quick_sort(int l, int r, int k) {
    if(l >= r) return a[k];

    int x = a[l], i = l - 1, j = r + 1;
    while (i < j) {
        do i++; while (a[i] < x);
        do j--; while (a[j] > x);
        if (i < j) swap(a[i], a[j]);
    }
    if (k <= j) return quick_sort(l, j, k);
    else return quick_sort(j + 1, r, k);
}

int main() {
    int n, k;
    cin >> n >> k;
    a = vector<int>(n, 0);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    cout << quick_sort(0, n - 1, k - 1) << endl;

    return 0;
}

在这个代码中,我们首先定义了一个常量MAXN来表示最大数据量,以及一个数组a来存储输入的正整数。接着,我们使用普通的cin来读取输入的数据。然后,我们使用sort函数对数组a进行快速排序,并使用unique函数对其进行去重操作。最后,我们调用quick_sort函数来查找第k个最小整数。在函数内部,我们使用快速排序算法来进行查找,并使用p来记录分界点左边的元素数量。如果k小于等于p,则递归调用quick_sort函数来查找第k个最小整数。否则,我们递归调用quick_sort函数来查找第k - p个最小整数。

4.7分治算法-棋盘覆盖问题

在一个2k×2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖

在这里插入图片描述

分治递归

#include <iostream>
using namespace std;

const int MAXN = 2050;
int board[MAXN][MAXN]; // 棋盘
int tile = 1; // L型骨牌编号

// 分治递归函数
void solve(int tr, int tc, int dr, int dc, int size) {
    if (size == 1) return; // 当前棋盘已经覆盖完毕
    int t = tile++; // 当前使用的L型骨牌编号
    int s = size / 2; // 棋盘大小的一半

    // 对左上角棋盘进行分治覆盖
    if (dr < tr + s && dc < tc + s)
        solve(tr, tc, dr, dc, s);
    else {
        board[tr + s - 1][tc + s - 1] = t;
        solve(tr, tc, tr + s - 1, tc + s - 1, s);
    }

    // 对右上角棋盘进行分治覆盖
    if (dr < tr + s && dc >= tc + s)
        solve(tr, tc + s, dr, dc, s);
    else {
        board[tr + s - 1][tc + s] = t;
        solve(tr, tc + s, tr + s - 1, tc + s, s);
    }

    // 对左下角棋盘进行分治覆盖
    if (dr >= tr + s && dc < tc + s)
        solve(tr + s, tc, dr, dc, s);
    else {
        board[tr + s][tc + s - 1] = t;
        solve(tr + s, tc, tr + s, tc + s - 1, s);
    }

    // 对右下角棋盘进行分治覆盖
    if (dr >= tr + s && dc >= tc + s)
        solve(tr + s, tc + s, dr, dc, s);
    else {
        board[tr + s][tc + s] = t;
        solve(tr + s, tc + s, tr + s, tc + s, s);
    }
}

int main() {
    int k, x, y;
    cin >> k >> x >> y; // 特殊方格的坐标

    // 初始化棋盘
    int size = 1 << k; // 棋盘大小为2^k
    for (int i = 0; i < size; i++)
        for (int j = 0; j < size; j++)
            board[i][j] = -1;

    // 设置特殊方格
    board[x][y] = 0;

    // 分治递归覆盖棋盘
    solve(0, 0, x, y, size);

    // 输出结果
    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            cout << board[i][j] << "\t";
        }
        cout << endl;
    }

    return 0;
}
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烟雨平生9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值