算法竞赛进阶指南——递归与递推

算法竞赛进阶指南——递归与递推


分治(分而治之):

分治法把一个问题分为若干个规模更小的同类子问题,对这些子问题递归求解,然后在回溯时通过它们推导出原问题的解。

例题(归并排序):https://www.acwing.com/problem/content/789/

给定你一个长度为 n 的整数数列。请你使用归并排序对这个数列按照从小到大进行排序。并将排好序的数列按顺序输出。

1 ≤ n ≤ 100000,所有整数均在 1∼10^9 范围内

image-20241201142838221

上图是归并排序的过程。结合上图,我们可以这样思考:

对于一个数列,我们将它一分为二。如果左边的序列和右边的序列是排好序的,我们就可以通过双指针在O(n)的时间复杂度内将整个数组排序好;反之,如果两边的序列都不是有序的,我们就无法这么做。这就像把一个问题拆成了两个同类的子问题,即左半边的序列的排序问题和右半边序列的排序问题。

我们将左右两半边排好序之后去解决当前问题,也就是可以使用O(n)的时间复杂度(双指针)将整个数列排好序。

这样的思考过程,就是将一个问题分解为更好解决的同类子问题。之后一直分解下去,直到基准问题。随后在回溯的过程中,我们解决每一个子问题,最后解决了整个问题。我认为递归(分治):就是将一个问题拆解成一些子问题,通过子问题逐步分解到基准问题得以解决,随后将这些子问题得到解决,从而大的问题也得到了解决。

什么是回溯?我们可以想象一个递归树,在函数向下递归到叶子节点而无法继续向深处进行时,其被return,也就是达到了基准问题。随后返回上一层函数调用的过程称为回溯。有时并非到达基准问题,也可以是被剪枝。

根据下图,显然达到了叶子节点8时,函数return,之后回溯到上一层。之后递归执行右分支,再回溯到上一层。在此回溯的过程中,显然,我们可以将这两个数(区间)合并成有序的。在之后所有回溯的过程中,我们都将每两个有序区间合并成一个区间并合并成一个新的有序区间。而合并的过程显然是O(n)的,再结合一分为二的过程,整体复杂度为O(nlogn)。

image-20241201154450185

关于回溯时,我们不仅可以解决问题,有时也需要维护一些状态在回溯前后保持不变。在维护时往往出现一些问题:我们有时需要修改维护,有时不需要。这是因为:除了全局变量或者引用传递,在函数递归调用时,函数内部维护的一些变量和参数,在结束后都会删除,不会影响到上一层;但如果是全局变量或者引用传递,回溯到上一层时这些变量或别的状态并不会跟着回溯(改变),所以我们要重新进行维护。

归并排序:

const int N = 100010;
int temp[N];
merge_sort(int a[], int l, int r) {
    if (l >= r) return;
    int mid = l + r >> 1;
    merge_sort(a, l, mid);
    merge_sort(a, mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) temp[k ++] = a[i ++];
        else temp[k ++] = a[j ++];
	}
    while (i <= mid) temp[k ++] = a[i ++];
    while (j <= r) temp[k ++] = a[j ++];
    for (int i = l, j = 0; i <= r; i ++, j ++) {
        a[i] = temp[j];
	}
}
例题(逆序对的数量):https://www.acwing.com/problem/content/790/

给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i < j 且 a[i] > a[j],则其为一个逆序对;否则不是。

1≤ n ≤ 100000

数列中的元素的取值范围 [1, 10^9]

在归并排序的过程中,我们可以同时计算出逆序对的数量。

一个数列逆序对的数量等于左半边逆序对的数量加右半边逆序对的数量加一个数在左半边另一数在右半边逆序对的数量。

当被分成单个数时,显然,逆序对的数量是0;也就是区间[8]和区间[3]的逆序对的数量都是0。

image-20241201160801531


image-20241201160949095

当向上回溯时,观察上图,我们发现在合并成区间[3, 8]的过程中,左边是有序的,右边是有序的。所以一个数在左半边,另一个数在右半边的逆序对的数量(如果可以形成),就是左半边区间从8开始到尽头的数量。(原因是:8 > 3形成了逆序对,又因为左边是从小到大排好序的,那么在8之后的数显然比3都大。)因此逆序对的数量可以在两个区间合并时得到。

其次,显然,左右两边是否排序不影响一个数在左半边,另一个数在右半边的逆序对的数量。

核心代码:

using LL = long long;
    
const int N = 100010;
int temp[N];

LL merge_sort(int l, int r) {
    if (l >= r) return 0;
    int mid = l + r >> 1;
    LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) temp[k ++] = a[i ++];
        else {
            res += mid - i + 1;
            temp[k ++] = a[j ++];
		}
	}
    while (i <= mid) temp[k ++] = a[i ++];
    while (j <= r) temp[k ++] = a[j ++];
    for (int i = l, j = 0; i <= r; i ++, j ++) {
        a[i] = temp[j];
    }
    return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值