分治思想

算法介绍

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

适用情况

  1. 该问题的规模缩小到一定的程度就可以容易地解决

  2. 该问题可以分解为若干个规模较小的相同问题。

  3. 利用该问题分解出的子问题的解可以合并为该问题的解。

实现步骤

  1. 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  2. 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  3. 合并:将各个子问题的解合并为原问题的解。

经典例题

汉诺塔

传说印度古代某寺庙有一个梵塔,塔内有三个座A、B、C,座A上放着n个大小不等的盘,其中大盘在下,小盘在上。有一个和尚想把这64个盘从座A搬到座B,但一次只能搬动一个盘,搬动的盘只允许放到其他两个座上,且大盘不能压在小盘上。
现要求用程序模拟该过程,并输出搬动步骤。

问题分析

  1. 如果n=1,可直接从座A搬动到座B。
  2. 如果n>=2,先将上面n-1个盘子搬到座C,再将最下面第n个盘子搬到座B,再将n-1个盘子从座C搬到座B。

代码实现

void Hanoi(int n,char A,char B,char C)
{
    if(n==1) printf("将第1个盘从%c搬到%c\n",A,B);
    //如果只有1个盘,A->B
    else
    {
        Hanoi(n-1,A,C,B);
        //先把 最上面的所有盘 A->C,移动过程会使用到B
        printf("将第%d个盘从%c搬到%c\n",n,A,B);
        //把最下边的盘 A->B
        Hanoi(n-1,C,B,A);
        //把C塔的所有盘 从C->B , 移动过程使用到座A
    }
}

最近平面点对

给定平面上 n 个点,找出其中的一对点的距离,使得在这 n个点的所有点对中,该距离为所有点对中最小的。

样例输入

第一行一个整数 n,表示点的个数。接下来 n行,每行两个实数x,y ,表示一个点的行坐标和列坐标。

3
1 1
1 2
2 2

样例输出

仅一行,一个实数,表示最短距离,四舍五入保留 4位小数。

1.000

问题分析

如果直接暴力,即枚举每个点到其他点的距离并更新距离的最小值。时间复杂度为O(n^2),在数据范围较小时可以考虑,但数据范围一大就会超时。

分治法求解

  1. 分解:将所有的点按照x坐标从小到大排序,以中间点mid为界划分为左右两个区域。
  2. 解决:递归寻找两个集合的最近点对。取两个集合最近点对的最小值d=min(dis_left,dis_right)
  3. 合并:这里需要考虑3种情况。
    1.最近点对位于左半边区域
    2.最近点对位于右半边区域
    3.两个点一个位于左半边,一个位于右半边。

    前两种情况均可通过递归左右区域完成,对于第三种情况我们可以这样考虑:
    如果存在第3种情况,则可以利用第1,2种情况得到的最小值d,以mid为界得到区间(mi
    d-d,mid+d),

    设P点是位于(mid-d,mid]的一个点,则在(mid,mid+d)中最多只有6个点与之匹配。
    以下为证明过程

    将右半边的矩形划分为6个相等的小矩形,其中每个矩形的对角线长s(即最大距离))由勾股定理得s=(25/36)d^2,s<d,由反证法,如果有7个点的话由抽屉原理必然有一个小矩形内存在两个点,那么这两个点的距离最大为s,又s<d,因此与之前所得到的左右区域最小值为d矛盾,因此最多只有6个点

时间复杂度

在分解和合并时,可能存在按照x轴、y轴进行排序的预处理O(nlogn),该问题在解决阶段只做提取的操作为Θ(n),计算后得到整体时间复杂度为:O(nlogn^2))

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,INF=1e10;
struct node
{
    double x,y;
}points[N],temp[N];

bool cmp1(node a,node b){return a.x<b.x;}
bool cmp2(node a,node b){return a.y<b.y;}

double dist(node a,node b)
{
    double dx=a.x-b.x,dy=a.y-b.y;
    return sqrt(dx*dx+dy*dy);
}

double slove(int l,int r)
{
    if(l>=r) return INF;//如果只有一个点,返回无穷大
    int mid=l+r>>1;//每次递归找中点
    double mid_x=points[mid].x;
    double res=min(slove(l,mid),slove(mid+1,r));//递归找左右区域的最小值
    int k=0;
    for(int i=l;i<=r;i++)
        if(points[i].x>=mid_x-res&&points[i].x<=mid_x+res)
            //如果在x轴坐标在(mid-res,mid+res)内就加入备选数组
                temp[k++]=points[i];
    sort(temp,temp+k,cmp2);//排序y轴
    for(int i=0;i<k;i++)
        for(int j=i+1;j<k&&temp[i].y-temp[j].y<res;j++)
            //如果y轴距离也在res内则更新最小值
            res=min(res,dist(temp[i],temp[j]));
    return res;
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    scanf("%lf%lf",&points[i].x,&points[i].y);
    sort(points,points+n,cmp1);//先按x轴排序
    printf("%.4lf",slove(0,n-1));
    return 0;
}

如果数据范围过大,我们还可以利用归并排序来排序y轴将时间复杂度降低到O(nlogn)

分形

分形,具有以非整数维形式充填空间的形态特征。通常被定义为“一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。

问题描述

分形

问题分析

  1. 分解:对于n>=2,我们只需要得到每个图形左上角的图形,再将其放到指定的位置即可。
  2. 解决:当n=1时可以直接输出,n>=2时可以通过递归求解。
  3. 合并:最后将各个子问题的解合并得到最终解。

我们可以发现:对于第1级分形它的长度len=1,第2级为3,第3级为9,因此第n级分形的长度为3^(n-1)。只要通过左上角图形的len确定其他四个部分的坐标,最后输出即可。

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=1000;
char g[N][N];
void dfs(int n)
{
    if(n==1)
    {
        g[0][0]='X';//如果只有1级直接返回'X'
        return ;
    }
    dfs(n-1);//递归找它的上一级
    int len=1;//确定第n-1级的长度
    for(int i=0;i<n-2;i++) len*=3;//输出第n-1级的图形
    int sx[4]={0,1,2,2},sy[4]={2,1,0,2};
    for(int k=0;k<4;k++)
        for(int i=0;i<len;i++)
            for(int j=0;j<len;j++)
                g[sx[k]*len+i][sy[k]*len+j]=g[i][j];
                //确定剩下四个部分的位置就确定了第n级
}
int main()
{
    dfs(7);
    int n;
    while(~scanf("%d",&n)&&n!=-1)
    {
        int len=1;//找到第n级的长度
        for(int i=0;i<n-1;i++) len*=3;
        //第1级长度为1,第2级为3,第3级为9....
        for(int i=0;i<len;i++)
        {
            for(int j=0;j<len;j++)
            {
                if(g[i][j]=='X') printf("X");
                else printf(" ");
            }
            printf("\n");
        }
        printf("-\n");
    }
    return 0;
}

CDQ分治

CDQ分治是我们处理各类问题的重要武器。它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;缺点在于必须离线操作

基本思想

  1. 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间[L,R]表示。
  2. 递归处理左边区间[L,M]和右边区间[M+1,R]的问题。
  3. 合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。  

这就是CDQ分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,[L,M]内的问题不会对[M+1,R]内的问题产生影响。

基本步骤

  1. 将整个序列分成长度相等的两部分
  2. 递归处理前一部分的子问题
  3. 计算前一部分的子问题中的修改操作对后一部分子问题的影响
  4. 递归处理后一部分的子问题

经典例题

三维偏序

前置知识:

  1. 树状数组(用来求前缀和)
  2. 归并排序,树状数组求逆序对
  3. 前缀和

题目描述


有 n 个元素,第 i 个元素有ai​,bi​,ci​ 三个属性,设 f(i) 表示满足aj​≤ai​ 且 bj​≤bi​ 且cj​≤ci​ 且 j!=i 的 j 的数量。对于d∈[0,n),求 f(i) = d 的数量。

输入格式

第一行两个整数 n,k,表示元素数量和最大属性值。接下来 n 行,每行三个整数 ai​,bi​,ci​,分别表示三个属性值。

输出格式

n 行,第 d+1 行表示 f(i)=d 的 i 的数量。

输入样例

10 3
3 3 3
2 3 3
2 3 1
3 1 1
3 1 2
1 3 1
1 1 2
1 2 2
1 3 2
1 2 1

输出样例

3
1
3
0
1
0
1
0
0
1

我们先来考虑一下二维偏序

二维偏序也就是aj​≤ai​,bj​≤bi​,应该怎么做

二维偏序解法

先按a为第一关键字排序,再按b为第二关键字排序
于是对于每一个i,只有1—i-1的元素会对它有贡献,那么我们只要查询1—i-1里bj<bi的元素个数就好了,其中利用树状数组动态维护b即可,或者归并排序求顺序对也行。

三维偏序思路

在三维偏序下,我们需要保证前两维都已经满足的情况下(即aj<ai,bj<bi,j<i)才能考虑第三维.
我们以a为第一关键字排序,b为第二关键字排序,c为第三关键字排序,第一维保证左边小于等于右边了,为了保证第二维也是左边小于右边,我们需要对第二维进行排序,第二维排序利用CDQ分治,第三维用树状数组维护。

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,tot1,k,tot2,anss[N],tree[N];
//s2排序处理第一维,cdq处理第二维,树状数组处理第三维
struct node
{
    int a,b,c,cnt,ans;
}s1[N],s2[N];//原数组,去重后的数组
bool cmp1(node x,node y)//排序第一维
{
	if(x.a==y.a)
	{
		if(x.b==y.b)return x.c<y.c;
		return x.b<y.b;
	}
	return x.a<y.a;
}
bool cmp2(node x,node y)//排序第二维
{
	if(x.b==y.b) return x.c<y.c;
	return x.b<y.b;
}
int lowbit(int x){return x&(-x);}//取出x的最低位1
void add(int x,int y)//区间修改
{
    while(x<=k)
    {
        tree[x]+=y;
        x+=lowbit(x);
    }
}
int query(int x)//单点查询
{
    int sum=0;
    while(x)
    {
        sum+=tree[x];
        x-=lowbit(x);
    }
    return sum;
}
void cdq(int l,int r)
{
    if(l==r) return ;//和归并排序很像
    int mid=l+r>>1;
    cdq(l,mid);
    cdq(mid+1,r);
    sort(s2+l,s2+1+mid,cmp2);//排序第二关键字
    sort(s2+1+mid,s2+r+1,cmp2);
    int j=l;
    for(int i=mid+1;i<=r;i++)
    {
        while(s2[i].b>=s2[j].b&&j<=mid)
        {
            add(s2[j].c,s2[j].cnt);//说明s2[j]对s2[i]有贡献,所以用树状数组处理c的时候在s2[j].c的位置上加上s2[j]的个数
            j++;
        }
        s2[i].ans+=query(s2[i].c);
        //对于s2[i]的贡献只要求出在s2[i].c之前的点的贡献之和
    }
    for(int i=l;i<j;i++)
    add(s2[i].c,-s2[i].cnt);//加上一个负的cnt从而清空树状数组
    //memset会超时
}
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    scanf("%d%d%d",&s1[i].a,&s1[i].b,&s1[i].c);
    sort(s1+1,s1+1+n,cmp1);
    for(int i=1;i<=n;i++)
    {
        tot1++;//去重,不然会出问题
        if(s1[i].a!=s1[i+1].a||s1[i].b!=s1[i+1].b||s1[i].c!=s1[i+1].c)
        {
            tot2++;
            s2[tot2].a=s1[i].a,s2[tot2].b=s1[i].b,s2[tot2].c=s1[i].c;
            s2[tot2].cnt=tot1;
            tot1=0;
        }
    }
    cdq(1,tot2);//注意不是n
    for(int i=1;i<=tot2;i++)
        anss[s2[i].ans+s2[i].cnt-1]+=s2[i].cnt;
    /*
    s2[i].ans表示的是aj<ai,bj<bi,cj<ci,但不包括aj=ai,bj=bi,cj=ci的j的个数
    而a[i].cnt恰好表示了aj=ai,bj=bi,cj=ci的个数,于是a[i].ans+a[i].cnt就是去重后第i个点的答案
    */
    for(int i=0;i<n;i++)
    printf("%d\n",anss[i]);//输出答案为d的i的数量
    return 0;
}
题目出处 题目类型 题目难度
AcWing 107 分治思想
洛谷P1115 分治思想
洛谷P1908 分治思想 ⭐⭐
POJ2503 分治思想 ⭐⭐
洛谷P1228 分治思想 ⭐⭐⭐
POJ 2366 分治思想 ⭐⭐⭐
POJ 1836 分治思想 ⭐⭐⭐
POJ 3122 分治思想 ⭐⭐⭐
洛谷P1498 分形 ⭐⭐
AcWing 118 分形 ⭐⭐
AcWing 98 分形 ⭐⭐⭐
POJ 3768 分形 ⭐⭐⭐
AcWing 119 平面最近点对 ⭐⭐⭐
洛谷P1429 平面最近点对 ⭐⭐⭐
洛谷P7883 平面最近点对 ⭐⭐⭐
洛谷P2717 CDQ分治 ⭐⭐⭐
洛谷P2345 CDQ分治 ⭐⭐⭐
洛谷P3810 CDQ分治 ⭐⭐⭐⭐
洛谷P4169 CDQ分治 ⭐⭐⭐⭐
洛谷P5459 CDQ分治 ⭐⭐⭐⭐
posted @ 2023-04-23 16:43  menitrust  阅读(26)  评论(0编辑  收藏  举报