从0开始学算法——第十八天(分治算法)

写在开头的话

学习了昨天的top k问题,今天让我们一起学习分治算法吧

第一节

知识点:

(1)分治思想(2)递归思想(3)汉诺塔问题

分治思想

简单介绍

分治思想是一种重要的问题解决方法,它将一个复杂的问题分解成多个相似的子问题来解决,然后将这些子问题的解合并起来得到原问题的解。这个过程包括三个主要步骤:分解、解决和合并。

分解:将原始问题划分成若干个规模较小的子问题。这一步骤需要考虑如何将问题划分为相互独立、互不重叠的子问题,通常通过递归的方式实现。每个子问题的规模都比原问题小,但是解决它们的方法与解决原问题的方法相同。

解决:通过递归地解决每个子问题,直到子问题的规模足够小,可以直接求解。

合并:将子问题的解合并成原始问题的解。这一步通常是最简单的,只需将各个子问题的解按照一定规则合并即可。

图示

分治思想的应用

归并排序:归并排序是分治思想的一个典型应用。它将一个数组划分成两个规模相等的子数组,分别对这两个子数组进行归并排序,然后将排好序的子数组合并成一个有序数组。归并排序的时间复杂度为 O(nlogn)。

快速排序:快速排序也是分治思想的一种应用,它通过选择一个基准元素将数组分割成两个子数组,然后递归地对子数组进行排序。快速排序的时间复杂度也为 O(nlogn)。

二分查找:二分查找是一种分治思想的典型应用,它通过将查找范围分成两部分,然后确定目标值在哪一部分,从而将查找范围缩小一半。这种方法的时间复杂度为 O(logn)。是非常常用的查找技巧。

递归思想

简单介绍

递归思想是一种解决问题的方法,它将问题分解为规模更小的相似子问题,并通过不断调用自身来解决这些子问题。递归通常包含两个部分:基本情况和递归步骤。基本情况是指能够直接求解的最简单情况,递归步骤则是指将问题分解为更小规模的子问题。

递归的两个组成部分

基本情况(Base Case):递归算法必须包含一个或多个基本情况,这些情况不需要再次递归调用就可以直接求解。基本情况通常是递归函数停止递归的条件,它们防止递归无限进行下去,确保算法能够结束。

递归步骤(Recursive Step):递归算法通过调用自身来解决规模更小的子问题。在每次递归调用中,问题的规模都会减小,直到达到基本情况,然后开始回溯(即从递归调用中返回)。

汉诺塔问题

让我们通过汉诺塔问题详细介绍分治思想和递归思想的应用。

问题描述

汉诺塔问题是计算机科学中经典的递归问题之一。问题描述如下:假设有三根柱子(分别记为A、B、C),A柱子上有 n 个不同大小的圆盘,按照从小到大的顺序堆叠。要求将这些圆盘从A柱子移动到C柱子,并保持它们的顺序不变,同时不能跨越柱子移动,所以需要借助B柱子作为中转。在移动过程中,要求任意时刻大圆盘在下,小圆盘在上。

代码实现
C++代码实现
cpp 复制代码
#include <iostream>
using namespace std;

void moveDisk(int n, char source, char dest, char aux) {
    if (n == 1) {
        cout << "Move disk 1 from " << source << " to " << dest << endl;
        return;
    }
    moveDisk(n - 1, source, aux, dest);
    cout << "Move disk " << n << " from " << source << " to " << dest << endl;
    moveDisk(n - 1, aux, dest, source);
}

void hanoi(int n, char source, char dest, char aux) {
    moveDisk(n, source, dest, aux);
}

int main() {
    int n = 3; // 例如,移动3个圆盘
    hanoi(n, 'A', 'C', 'B');
    return 0;
}
Java代码实现
java 复制代码
public class HanoiTower {
    public static void moveDisk(int n, char source, char dest, char aux) {
        if (n == 1) {
            System.out.println("Move disk 1 from " + source + " to " + dest);
            return;
        }
        moveDisk(n - 1, source, aux, dest);
        System.out.println("Move disk " + n + " from " + source + " to " + dest);
        moveDisk(n - 1, aux, dest, source);
    }

    public static void hanoi(int n, char source, char dest, char aux) {
        moveDisk(n, source, dest, aux);
    }

    public static void main(String[] args) {
        int n = 3; // 例如,移动3个圆盘
        hanoi(n, 'A', 'C', 'B');
    }
}
Python代码实现
python 复制代码
def move_disk(n, source, dest, aux):
    if n == 1:
        print("Move disk 1 from", source, "to", dest)
        return
    move_disk(n - 1, source, aux, dest)
    print("Move disk", n, "from", source, "to", dest)
    move_disk(n - 1, aux, dest, source)

def hanoi(n, source, dest, aux):
    move_disk(n, source, dest, aux)

if __name__ == "__main__":
    n = 3  # 例如,移动3个圆盘
    hanoi(n, 'A', 'C', 'B')
运行结果

简单总结

本节详细了解了下分治思想和递归思想,并通过汉诺塔问题应用了展示了以上的思想应该如何应用。

第二节

知识点:

(1)传统乘法(2)分治法优化

传统乘法

具体步骤

传统乘法的原理基于逐位相乘和累加的过程。具体步骤如下:

  1. 逐位相乘:将一个数的每一位分别与另一个数的每一位相乘,得到若干个部分积(partial products)。每个部分积相当于一个数乘以另一个数的一位(并考虑其权重,即位数)。

  2. 累加部分积:将所有部分积按照其对应的位数进行累加。部分积的结果按照它们的位数进行排列和相加。

例子:123×45

复制代码
      1 2 3
    ×   4 5
    _______
      6 1 5  (123 × 5)
  + 4 9 2 0  (123 × 40, 因为是乘以4,并且要向左移一位)
    _______
    5 5 3 5
详细步骤:
  1. 逐位相乘

    • 首先,取45的个位数5,与123的每一位相乘:123×5=615
    • 然后,取45的十位数4(相当于40),与123的每一位相乘: 123×40=4920
  2. 累加部分积

    • 将两个部分积615和4920按照它们的位数对齐,然后相加

传统乘法的复杂度为O(),其中n是数字的位数。这是因为每一位的乘法操作需要与另一数的每一位进行相乘,并最终累加所有部分积。

尽管传统乘法方法简单且易于理解,但在处理大数时效率较低,因为操作次数随着数字位数的平方增长。因此,为了提高效率,在实际应用中往往需要更复杂的优化算法。

代码实现
C++代码实现
cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

string multiply(string num1, string num2) {
    int m = num1.size(), n = num2.size();
    vector<int> result(m + n, 0);

    for (int i = m - 1; i >= 0; --i) {
        for (int j = n - 1; j >= 0; --j) {
            int mul = (num1[i] - '0') * (num2[j] - '0');
            int p1 = i + j, p2 = i + j + 1;
            int sum = mul + result[p2];

            result[p1] += sum / 10;
            result[p2] = sum % 10;
        }
    }

    string res;
    for (int num : result) {
        if (!(res.empty() && num == 0)) {
            res.push_back(num + '0');
        }
    }

    return res.empty() ? "0" : res;
}

int main() {
    string num1 = "123";
    string num2 = "45";
    cout << "C++ Result: " << multiply(num1, num2) << endl;
    return 0;
}
Java代码实现
java 复制代码
public class MultiplyStrings {

    public static String multiply(String num1, String num2) {
        int m = num1.length(), n = num2.length();
        int[] result = new int[m + n];

        for (int i = m - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0');
                int p1 = i + j, p2 = i + j + 1;
                int sum = mul + result[p2];

                result[p1] += sum / 10;
                result[p2] = sum % 10;
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int num : result) {
            if (!(sb.length() == 0 && num == 0)) {
                sb.append(num);
            }
        }
        return sb.length() == 0 ? "0" : sb.toString();
    }

    public static void main(String[] args) {
        String num1 = "123";
        String num2 = "45";
        System.out.println("Java Result: " + multiply(num1, num2));
    }
}
Python代码实现
python 复制代码
def multiply(num1, num2):
    m, n = len(num1), len(num2)
    result = [0] * (m + n)

    for i in range(m - 1, -1, -1):
        for j in range(n - 1, -1, -1):
            mul = int(num1[i]) * int(num2[j])
            p1, p2 = i + j, i + j + 1
            sum = mul + result[p2]

            result[p1] += sum // 10
            result[p2] = sum % 10

    result_str = ''.join(map(str, result))
    return result_str.lstrip('0') or '0'

# Test
num1 = "123"
num2 = "45"
print(f"Python Result: {multiply(num1, num2)}")
运行结果(呈现C++的结果)

分治法优化

卡拉茨巴算法(Karatsuba Algorithm)是一种用于大整数乘法的快速算法。它通过将乘法分解为多个较小的子问题来减少总的乘法运算次数。其时间复杂度为,比传统的竖式乘法的O()复杂度要高效得多。

算法原理

卡拉茨巴算法的基本思想是分治法,将两个大数分解成较小的部分,从而减少乘法运算的次数。

假设有两个 n 位数X和Y,我们将它们表示为:

其中X1​和Y1​是高位部分,X0​和Y0​是低位部分,m是位数的一半。

根据上述表示法,乘积X×Y可以写成:

减少乘法次数

为了减少乘法运算次数,我们引入中间变量:

注意到P3可以展开为:

于是我们有:

最终,乘积X×Y可以表示为:

通过这种方式,我们将原来的四次乘法(传统竖式乘法)减少为三次乘法(卡拉茨巴算法),从而提高了计算效率。

递归实现

卡拉茨巴算法通常采用递归方式实现。对于每一层递归,我们都将问题分解成更小的子问题,直到子问题足够小,可以直接用简单的乘法解决。

示例

让我们通过一个具体的例子来理解卡拉茨巴算法。

假设我们要计算 1234×5678:

因此,1234×5678=7006652。

卡拉茨巴算法通过减少乘法次数来提高大数乘法的计算效率,特别适合大数的乘法运算。它的递归思想和分治法使得其在处理大数乘法时比传统方法更高效。

代码实现
C++代码实现
cpp 复制代码
#include <bits/stdc++.h>

// Pad the string with zeros on the left to make its length equal to 'length'
std::string padZero(const std::string &str, int length) {
    std::string result = str;
    while (result.length() < length) {
        result = "0" + result;
    }
    return result;
}

// Karatsuba algorithm for multiplying two large numbers represented as strings
std::string karatsuba(const std::string &x, const std::string &y) {
    int n = std::max(x.size(), y.size());
    if (n == 1) {
        return std::to_string((x[0] - '0') * (y[0] - '0'));
    }
    if (n % 2 != 0) {
        n++;
    }

    std::string xPadded = padZero(x, n);
    std::string yPadded = padZero(y, n);
    int m = n / 2;

    std::string x1 = xPadded.substr(0, m);
    std::string x0 = xPadded.substr(m, n - m);
    std::string y1 = yPadded.substr(0, m);
    std::string y0 = yPadded.substr(m, n - m);

    std::string P1 = karatsuba(x1, y1);
    std::string P2 = karatsuba(x0, y0);
    std::string P3 = karatsuba(std::to_string(std::stoi(x1) + std::stoi(x0)), std::to_string(std::stoi(y1) + std::stoi(y0)));

    int P1Int = std::stoi(P1);
    int P2Int = std::stoi(P2);
    int P3Int = std::stoi(P3);
    int P3MinusP1MinusP2 = P3Int - P1Int - P2Int;

    std::string result = std::to_string(P1Int * pow(10, n) + P3MinusP1MinusP2 * pow(10, m) + P2Int);
    return result;
}

int main() {
    std::string x = "1234";
    std::string y = "5678";
    std::string result = karatsuba(x, y);
    std::cout << "Product: " << result << std::endl;
    return 0;
}
Java代码实现
java 复制代码
public class Karatsuba {
    public static String padZero(String str, int length) {
        StringBuilder result = new StringBuilder(str);
        while (result.length() < length) {
            result.insert(0, "0");
        }
        return result.toString();
    }

    public static String karatsuba(String x, String y) {
        int n = Math.max(x.length(), y.length());
        if (n == 1) {
            return Integer.toString((x.charAt(0) - '0') * (y.charAt(0) - '0'));
        }
        if (n % 2 != 0) {
            n++;
        }

        x = padZero(x, n);
        y = padZero(y, n);
        int m = n / 2;

        String x1 = x.substring(0, m);
        String x0 = x.substring(m);
        String y1 = y.substring(0, m);
        String y0 = y.substring(m);

        String P1 = karatsuba(x1, y1);
        String P2 = karatsuba(x0, y0);
        String P3 = karatsuba(Integer.toString(Integer.parseInt(x1) + Integer.parseInt(x0)), Integer.toString(Integer.parseInt(y1) + Integer.parseInt(y0)));

        int P1Int = Integer.parseInt(P1);
        int P2Int = Integer.parseInt(P2);
        int P3Int = Integer.parseInt(P3);
        int P3MinusP1MinusP2 = P3Int - P1Int - P2Int;

        return Integer.toString(P1Int * (int)Math.pow(10, n) + P3MinusP1MinusP2 * (int)Math.pow(10, m) + P2Int);
    }

    public static void main(String[] args) {
        String x = "1234";
        String y = "5678";
        String result = karatsuba(x, y);
        System.out.println("Product: " + result);
    }
}
Python代码实现
python 复制代码
def pad_zero(s, length):
    return s.zfill(length)

def karatsuba(x, y):
    n = max(len(x), len(y))
    if n == 1:
        return str(int(x) * int(y))
    if n % 2 != 0:
        n += 1

    x = pad_zero(x, n)
    y = pad_zero(y, n)
    m = n // 2

    x1, x0 = x[:m], x[m:]
    y1, y0 = y[:m], y[m:]

    P1 = karatsuba(x1, y1)
    P2 = karatsuba(x0, y0)
    P3 = karatsuba(str(int(x1) + int(x0)), str(int(y1) + int(y0)))

    P1Int = int(P1)
    P2Int = int(P2)
    P3Int = int(P3)
    P3MinusP1MinusP2 = P3Int - P1Int - P2Int

    result = P1Int * 10**n + P3MinusP1MinusP2 * 10**m + P2Int
    return str(result)

if __name__ == "__main__":
    x = "1234"
    y = "5678"
    result = karatsuba(x, y)
    print("Product:", result)
运行结果

简单总结

本节主要学习了大数乘法,以及分治法优化大数乘法。

相关推荐
算法与双吉汉堡2 小时前
【短链接项目笔记】Day2 用户注册
java·redis·笔记·后端·spring
LYFlied2 小时前
【每日算法】LeetCode 84. 柱状图中最大的矩形
前端·算法·leetcode·面试·职场和发展
CoderCodingNo2 小时前
【GESP】C++三级真题 luogu-B4414 [GESP202509 三级] 日历制作
开发语言·c++·算法
思成不止于此2 小时前
【MySQL 零基础入门】MySQL 约束精讲(一):基础约束篇
数据库·笔记·sql·学习·mysql
Liangwei Lin2 小时前
洛谷 P1955 [NOI2015] 程序自动分析
算法
小黄人软件3 小时前
【过度滥用眼】真正的理解,从闭眼开始:如何把“眼睛视觉依赖”降到最低,把大脑效率提到最高。【最少用眼的工作与学习体系】
学习
zwjapple3 小时前
全栈开发面试高频算法题
算法·面试·职场和发展
不穿格子的程序员3 小时前
从零开始写算法——链表篇5:K个一组翻转链表 + 排序链表
算法·链表·分治
青鸟2183 小时前
从资深开发到脱产管理的心态转变
后端·算法·程序员