当前位置: 首页 > article >正文

【算法】分治

分治

  • 1.逆序对
  • 2.求第 k 小的数
  • 3.最大子段和
  • 4.地毯填补问题

分治,字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

1.逆序对

P1908 逆序对

在这里插入图片描述

解法:分治

「分治」是解决「逆序对」⾮常经典的解法,也可以利用「树状数组」或「线段树」解决逆序对问题。

如果把整个序列 [l, r] 从中间 mid 位置分成两部分,那么逆序对个数可以分成三部分:

  1. [l, mid] 区间内逆序对的个数 c1。
  2. [mid + 1, r] 区间内逆序对的个数 c2。
  3. 从 [l, mid] 以及 [mid + 1, r] 各选一个数,能组成的逆序对的个数 c3。

那么逆序对的总数就是 c1 + c2 + c3。其中求解 c1,c2的时候跟原问题是一样的,可以交给「递归」去处理,那我们重点处理「一左一右」的情况。

如果在处理一左一右的情况时,两个数组「无序」,我们的时间复杂度其实并没有优化到哪去,甚至还「不如暴力解法」。但是如果两个数组有序的话,我们就可以快速找出逆序对的个数。

先不管怎么求逆序对,我们能让左右两个数组有序嘛?就是「归并排序」。因此,我们能做到在求完 c1, c2 之后,然后让「左右两个区间有序」。那么接下来问题就变成,已知两个「有序数组」,如何求出左边选一个数,右边选一个数的情况下的逆序对的个数。核心思想就是找到右边选一个数之后,左边区间内「有多少个比我大的」。

在这里插入图片描述

定义两个「指针」扫描两个有序数组,此时会有下面三种情况:

  1. a[cur1] ≤ a[cur2]:a[cur1] 不会与 [cur2, r] 区间内任何一个元素构成逆序对,cur1++。
  2. a[cur1] > a[cur2]:此时 [cur1, mid] 区间内所有元素都会与 a[cur2] 构成逆序对,逆序对个数增加,mid − cur1 + 1,此时 cur2 已经统计过逆序对了,cur2++。

重复上面两步,我们就可以在 O(N) 的时间内处理完「一左一右」时,逆序对的个数。而且,我们会发现,这跟我们「归并排序的过程」是高度一致的。所以可以一边排序,一边计算逆序对的个数。

#include<iostream>
using namespace std;

typedef long long LL;

const int N = 5e5 + 10;

int n;
int a[N];
int t[N]; //辅助数组

LL merge(int left, int right)
{
    if(left == right) return 0;
    
    LL ret = 0;
    int mid = (left + right) / 2;
    ret += merge(left, mid);
    ret += merge(mid + 1, right);
    
    //一左一右的情况
    int cur1 = left, cur2 = mid + 1, i = left;
    while(cur1 <= mid && cur2 <= right)
    {
        if(a[cur1] <= a[cur2]) t[i++] = a[cur1++];
        else
        {
            ret += mid - cur1 + 1;
            t[i++] = a[cur2++];
        }
    }
    while(cur1 <= mid) t[i++] = a[cur1++];
    while(cur2 <= right) t[i++] = a[cur2++];
    
    for(int j = left; j <= right; j++) a[j] = t[j];
    
    return ret;
}

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    cout << merge(1, n) << endl;
    
    return 0;
}

2.求第 k 小的数

P1923 【深基9.例4】求第 k 小的数

在这里插入图片描述

解法:分治

在快排中,当我们把数组「分成三块」之后: [l, left] [left + 1, right - 1] [right, r] ,我们可以通过计算每一个区间内元素的「个数」,进而推断出我们要找的元素是在「哪一个区间」里面。

那么我们可以直接去「相应的区间」去寻找最终结果就好了。

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

const int N = 5e6 + 10;

int n, k;
int a[N];

int GetRandom(int left, int right)
{
    return a[rand() % (right - left + 1) + left];
}

int QuickSelect(int left, int right, int k)
{
    if(left == right) return a[left];

    //1.随机选择基准值
    int key = GetRandom(left, right);

    //2.数组分三块
    int l = left - 1, r = right + 1, cur = left;
    while(cur < r)
    {
        if(a[cur] < key) swap(a[++l], a[cur++]);
        else if(a[cur] == key) cur++;
        else swap(a[cur], a[--r]);
    }

    //3.选择存在最终结果的区间
    //[left, l] [l + 1, r - 1] [r, right]
    int a = l - left + 1, b = r - 1 - l, c = right - r + 1;
    if(k <= a) return QuickSelect(left, l, k);
    else if(k <= a + b) return key;
    else return QuickSelect(r, right, k - a - b);
}

int main()
{
    srand(time(0));
    scanf("%d%d", &n, &k);
    k++;
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]); //cin、cout超时
    
    cout << QuickSelect(1, n, k) << endl;

    return 0;
}

3.最大子段和

P1115 最大子段和

在这里插入图片描述

解法:分治

如果把整个序列 [l, r] 从中间 mid 位置分成两部分,那么整个序列中「所有的子数组」就分成三部分:

  1. 子数组在区间 [l, mid] 内。
  2. 子数组在区间 [mid + 1, r] 内。
  3. 子数组的左端点在 [l, mid] 内,右端点在 [mid + 1, r] 内。

在这里插入图片描述

那么我们的「最终结果」也会在这三部分取到,要么在左边区间,要么在右边区间,要么在跨越中轴线的区间。因此,我们可以先求出左边区间的最大子段和,再求出右边区间的最大子段和,最后求出中间区间的最大子段和。其中求「左右区间」时,可以交给「递归」去解决。

那我们重点处理如何求出「中间区间」的最大子段和。可以把中间区间分成两部分:

  1. 左边部分是从 mid 为起点,「向左延伸」的最大子段和。
  2. 右边部分是从 mid + 1 为起点,「向右延伸」的最大子段和。

分别求出这两个值,然后相加即可。求法也很简单,直接「固定起点」,一直把「以它为起点的所有子数组」的和都计算出来,取最大值即可。

#include<iostream>
using namespace std;

const int N = 2e5 + 10;

int n;
int a[N];

int dfs(int left, int right)
{
    if(left == right) return a[left];
    
    int mid = (left + right) / 2;
    
    //先求出左右两边的最大值
    int ret = max(dfs(left, mid), dfs(mid + 1, right));
    
    //求出以a[mid]开始,向左延伸的最大值
    int sum = a[mid], lmax = a[mid];
    for(int i = mid - 1; i >= left; i--)
    {
        sum += a[i];
        lmax = max(lmax, sum);
    }
    //求出以a[mid+1]开始,向右延伸的最大值
    sum = a[mid + 1]; int rmax = a[mid + 1];
    for(int i = mid + 2; i <= right; i++)
    {
        sum += a[i];
        rmax = max(rmax, sum);
    }
    //求三者的最大值
    ret = max(ret, lmax + rmax);
    
    return ret;
}

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    cout << dfs(1, n) << endl;
    
    return 0;
}

4.地毯填补问题

P1228 地毯填补问题

在这里插入图片描述

解法:分治

非常经典的一道分治题目,也可以说是一道递归题目。

一维分治的时候,我们是从中间把整个区间切开,分成左右两部分(其实有时候我们可以三等分,就看具体问题是什么)。⼆维的时候,我们可以横着一刀竖着一刀,分成左上、右上、左下、右下四份。而这道题的矩阵长度正好是 2^k,能够被不断平分下去。像是在暗示我们,要用分治,要用分治,要用分治…

当我们把整个区间按照中心点一分为四后,四个区间里面必然有一个区间有缺口(就是公主的位置),那这四个区间不一样,那就没有相同子问题了。别担心,只要我们在中心位置放上一块地毯,四个区间就都有一个缺口了。如下图所示:

在这里插入图片描述

无论缺口在哪里,我们都可以在缺口对应的区间的角落,放上一个地毯。接下来四个区间都变成只有一个缺口的形式,就可以用递归处理子问题。因此,我们拿到一个矩阵后的策略就是:

  1. 先四等分。
  2. 找出缺口对面的区间,放上一块地毯。
  3. 递归处理四个子问题。
#include<iostream>
using namespace std;

int k, x, y;

//len: 区间的长度; (a, b): 区间的左上角坐标; (x, y): 障碍物的坐标
void dfs(int len, int a, int b, int x, int y)
{
    if(len == 1) return;
    
    len /= 2;
    if(x < a + len && y < b + len) //障碍物在左上角
    {
        cout << a + len << " " << b + len << " " << 1 << endl; //铺上1号地毯
        dfs(len, a, b, x, y);
        dfs(len, a, b + len, a + len - 1, b + len);
        dfs(len, a + len, b, a + len, b + len - 1);
        dfs(len, a + len, b + len, a + len, b + len);
    }
    else if(x >= a + len && y >= b + len) //障碍物在右下角
    {
        cout << a + len - 1 << " " << b + len - 1 << " " << 4 << endl; //铺上4号地毯
        dfs(len, a, b, a + len - 1, b + len - 1);
        dfs(len, a, b + len, a + len - 1, b + len);
        dfs(len, a + len, b, a + len, b + len - 1);
        dfs(len, a + len, b + len, x, y);
    }
    else if(x >= a + len) //障碍物在左下角
    {
        cout << a + len - 1 << " " << b + len << " " << 3 << endl; //铺上3号地毯
        dfs(len, a, b, a + len - 1, b + len - 1);
        dfs(len, a, b + len, a + len - 1, b + len);
        dfs(len, a + len, b, x, y);
        dfs(len, a + len, b + len, a + len, b + len);
    }
    else //障碍物在右上角
    {
        cout << a + len << " " << b + len - 1 << " " << 2 << endl; //铺上2号地毯
        dfs(len, a, b, a + len - 1, b + len - 1);
        dfs(len, a, b + len, x, y);
        dfs(len, a + len, b, a + len, b + len - 1);
        dfs(len, a + len, b + len, a + len, b + len);
    }
}

int main()
{
    cin >> k >> x >> y;
    k = (1 << k);
    dfs(k, 1, 1, x, y);
    
    return 0;
}

http://www.kler.cn/a/527529.html

相关文章:

  • 新鲜速递:DeepSeek-R1开源大模型本地部署实战—Ollama + MaxKB 搭建RAG检索增强生成应用
  • Python练习(3)
  • 随笔 | 写在一月的最后一天
  • 【Proteus仿真】【51单片机】多功能计算器系统设计
  • 使用 OpenResty 构建高效的动态图片水印代理服务20250127
  • riscv xv6学习笔记
  • 常用Android模拟器(雷电 MuMu 夜神 Genymotion 蓝叠) - 20250131
  • 基于LLM的垂直领域问答方案
  • 深入理解 C# 与.NET 框架
  • windows电脑运行日志用户行为记录查看和清理工具
  • 【字符串两大注意事项】
  • CF EDU ROUND 171
  • Memcached add 命令详解
  • 计算机网络之计算机网络的分类
  • 对有向无环图进行拓扑排序
  • FFmpeg在Ubuntu18.04上的安装
  • 本地化部署DeepSeek-R1
  • jdk8项目升级到jdk17——岁月云实战
  • 从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架(基础组件实现)
  • 51单片机开发——I2C通信接口
  • Keepalived高可用集群企业应用实例二
  • Qt事件处理:理解处理器、过滤器与事件系统
  • 【机器学习】Google开源大模型Gemma2:原理、微调训练及推理部署实战
  • R 字符串:深入理解与高效应用
  • 推荐一款好用的翻译类浏览器扩展插件
  • 11.QT控件:输入类控件