算法之分治思想

分治,与其说是一种算法,不如说是一种思想。所谓分治,就是将一个复杂的、规模大的问题分解成n个已解决的小问题。这些小问题彼此间独立且有统一的解决方式,这些小问题的解的总和即为原问题的解。
分治算法与迭代是分不开的,因为我们需要迭代来指数次分解原问题。因此,在使用分治算法解决问题是,我们要思考三个问题:

  • 将问题分解到怎样的规模,即最小子问题是怎样的
  • 如何使用统一的方法解决小问题
  • 如何将小问题的解合并为原问题的解

以我的经验来看,一般的编程题要求将子问题缩小为单个元素或两个元素,分解方式一般为“分两边”。分治算法应用的问题非常多,掌握分治算法要求我们大量的练习。我们通过下面的几个题目来了解分治思想,分治算法的经典题目有很多,除了下面的题目,大家可以自行寻找并进行练习。

全排列问题

给定一组数,打印出它们的全排列。
相信大家一定知道求一组数的全排列个数的公式——n!,即n * n-1 * n-2 * ……* 3 * 2 * 1。这个公式的意思是,n个数作为开头有n中开头,在开头确定后,剩下的n-1个数作为第二个又有n-1种,然后是第三个,第四个……一直到最后一个。对于上面的三个问题:

  • 剩下的每个数分别放当前前数列的开头(效果与剩下的每个数放最后相当),一直到迭代n次后只剩下最后一个数
  • 设置一个函数,用处是把当前的第一个数放到最后
  • 当迭代到只剩一个数时,打印当前数列

Talk is cheap,show me the code.

void lines(int n,int z[])
{
    if(n==Length)    /*当最后一位确定后打印数组z,Length是数组z的长度*/
    {
        for(int i=0;i<Length;i++)
        {
            printf("%2d",z[i]);
        }
        printf("\n");
    }
    for(int i=1;i<=Length-n;i++)    /*把剩下的每个数都放到最后一次,每次都确定了当前的位置上的数*/
    {
        int k=z[n];    /*将当前的第n个放到末尾*/
        for(int j=n;j<4;j++)
        {
            z[j]=z[j+1];
        }
        z[3]=k;
        lines(n+1,z);    /*确定下一个位置*/
    }
}

合并排序

合并排序是一种快速的排序方式,核心思想是不断地将两组在自己组内有序的数合并成为一组有序数,比如将1579和23468合并成为123456789。合并排序的最小子问题为将两个数,或一个数分为一组并进行排序。将两组数合并为一组的方法是在两组数里分别从小到大取数,放入新数组中对应的位置,比如1579和23468,先取1和2中较小的放在第一个,再取5和2中较小的放第二个,再取5和3中较小的放第三个……,这样遍历两个子数组就可以获得一个有序的数组。

void mergesort(int l,int r,int z[],int A[])
{
    if(l>=r) return;
    int k=(l+r+1)/2;    /*k为数组中间的位置*/
    mergesort(l,k-1,z,A);    /*使当前数组的左子数组和右子数组有序*/
    mergesort(k,r,z,A);
    int a=l,b=k,p=l;    /*a为遍历左子数组的标记,b为遍历右子数组的标记,p为合并数组的标记*/
    while(a<k&&b<=r)    /*遍历两个数组*/
    {
        if(z[a]<z[b])
        {
            A[p++]=z[a++];
        }
        else
        {
            A[p++]=z[b++];
        }
    }
    while(a<k)    /*当左子数组未完全遍历时将剩下的数合并入数组A*/
    {
        A[p++]=z[a++];
    }
    while(b<=r)     /*当右子数组未完全遍历时将剩下的数合并入数组A*/
    {
        A[p++]=z[b++];
    }
    for(int i=l;i<=r;i++)    /*将数组A中的数组复制到数组z中,完成两个子数组的合并*/
    {
        z[i]=A[i];
    }
}

可以发现,合并算法时从最底层的一个元素或两个元素进行合并,然后逐渐合并长度更大的数组,最后是整个数组有序。在迭代的过程中,程序迭代到底层再进行操作,再从最底层一步一步向上进行合并操作。下面有动图帮助大家理解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-myC0k0im-1582516023448)(https://images.xiaozhuanlan.com/photo/2019/93d3c4ea91973f13677897fefce11c70.gif)]

动图中最开始分组,即从上向下不停细分的过程在代码里也有体现:在程序运行时,先顺序运行程序,并在一开始就进行了迭代,对数组两个子数组分别进行操作,这时两个子数组是无序的,这就是动图中的“分组”过程。虽然没有改变元素在数组中的位置,但确实像动图里那样生成了“框”区分不同的数组。在运行到底层的合并操作后,再一层一层地上升,最终是这个数组有序。

棋盘覆盖问题

有一个2^k * 2^k的棋盘,上面有一个空格,问能否用若干块能覆盖三个格子的“L”型积木对棋盘进行覆盖(除了空格)?
对于这个问题,我们可以这样想:只要我能在棋盘上放上一块积木,我就离达成目标近了一步。这个问题与上一个合并排序有一个不同的地方是:合并排序要求解决当前问题的子问题后,利用子问题解决当前的问题,而棋盘覆盖问题要求对当前的问题进行操作,再操作当前问题的子问题。同样是一步一步地分解问题、解决问题,合并排序是先打散问题,通过不停地解决子问题逐渐从底层向上层求解;棋盘覆盖问题是一边操作问题一边分解问题,逐渐将问题操作到底层。直白来说,合并排序是从底层到顶层,是逆向解决;棋盘覆盖问题一开始就在处理原来的问题,从顶层处理到底层,是正向解决。 事实上,对于棋盘覆盖问题,我们很难像合并排序那样逆向求解,因为如果什么都不做就分隔棋盘,只是让棋盘变小,问题并没有变得简单,空格的特殊性在分隔后的子棋盘中依然存在。这是这个问题的难点,也是突破点。

void paint(int y,int x,int cy,int cx,int size)    /*(y,x)是特殊点的纵坐标,(cy,cx)是当前棋盘左上顶点的坐标,size表示当前棋盘的大小*/
{
    if(size==1) return;    /*处理到底层时结束*/
    int s=size/2;
    int a=t++;    /*变量t为全局变量*/
	/*对当前棋盘的左上子棋盘进行操作*/
	if(y<cy+s&&x<cx+s)
    {
        paint(y,x,cy,cx,s);    /*如果有特殊点就直接继续迭代*/
    }
    else
    {
        z[cy+s-1][cx+s-1]=a;    /*如果没有特殊点就把子棋盘靠近当前棋盘中心的顶点用积木覆盖并标为下一次迭代的特殊点,这样就获得了一个有特殊点的子棋盘,就可以用同样的方法进行操作*/
        paint(cy+s-1,cx+s-1,cy,cx,s);
    }
    /*对当前棋盘的右上子棋盘进行操作*/
	if(y<cy+s&&x>=cx+s)
    {
        paint(y,x,cy,cx+s,s);
    }
    else
    {
        z[cy+s-1][cx+s]=a;
        paint(cy+s-1,cx+s,cy,cx+s,s);
    }
   /*对当前棋盘的左下子棋盘进行操作*/
   if(y>=cy+s&&x<cx+s)
    {
        paint(y,x,cy+s,cx,s);
    }
    else
    {
        z[cy+s][cx+s-1]=a;
        paint(cy+s,cx+s-1,cy+s,cx,s);
    }
   /*对当前棋盘的右下子棋盘进行操作*/
   if(y>=cy+s&&x>=cx+s)
    {
        paint(y,x,cy+s,cx+s,s);
    }
    else
    {
        z[cy+s][cx+s]=a;
        paint(cy+s,cx+s,cy+s,cx+s,s);
    }
	/*四次操作,特殊点肯定在四个子棋盘中的一个里,剩下三个子棋盘被标记的顶点组成了一个“L”型积木*/
}

在一开始做棋盘覆盖问题的时候,我对于将棋盘分为四个子棋盘操作并不感到惊讶,事实上这很好理解。我感到困难的地方在于我想了很久如何表示四个子棋盘以及函数中的参量应该取哪几个(说实话一开始我只用了两个参量……)。这也让我了解到对于迭代的参量选取的准则:各个迭代过程都会用到的变量。 虽然总结出来只有一句话,而且这句话大家都懂,但是不多不少的找到函数中的参量真的不算容易,一旦找到应该有的参量,迭代就会变得简单。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值