分治

分治 基本思想 
分治:分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了。
但是我们应该在什么时候用分治呢?做题的时候就不知道用什么算法,能用分治法的基本特征:
1.问题缩小到一定规模容易解决
2.分解成的子问题是相同种类的子问题,即该问题具有最优子结构性质
3.分解而成的小问题在解决之后要可以合并
4.子问题是相互独立的,即子问题之间没有公共的子问题

第一条大多数问题都可以满足
第二条的大多数问题也可以满足,反应的是递归的思想
第三条:这个是能分治的关键,解决子问题之后如果不能合并从而解决大问题的话,那就不行,如果满足一,二,不满足三,即具有最优子结构(贪心DP特点)的话,可以考虑贪心或者dp
第四条:如果不满足第四条的话,也可以用分治,但是在分治的过程中,有大量的重复子问题被多次的计算,拖慢了算法效率,这样的问题可以考虑dp(大量重复子问题)

解决问题的三个步骤
1.分解成很多子问题
2.解决这些子问题
3.将解决的子问题合并从而解决整个大问题
化成一颗问题树的话,最底下的就是很多小问题,最上面的就是要解决的大问题,自底向上的方式求解问题。

例子:二分查找
条件是数组有序
1.暴力的做法就是拿跟数组里面每个数比较一下,有的话就返回下标,这个是大问题
2.每次将数组长度 分一半 ,前提已经有序,在前一半(后一半),那后一半(前一半)就不会找到了,最后把子问题合并起来,大问题解决了。

代码 ::
#include<stdio.h>
#include<iostream>
#include<string.h> 
#include<algorithm>
using namespace std;
const int maxn=100;
typedef long long ll;
int a[maxn];
void erfenchazhao(int str[maxn],int chanum,int left,int right)
{
    if(left>right)
    {
        cout<<"cannot find it"<<endl;
        return ;
    }    
    int mid=(left+right)/2.0;    
    if(chanum==str[mid])
        cout<<"find it"<<endl;
    else
        {
            if(chanum>str[mid])            
                erfenchazhao(str,chanum,mid+1,right);            
            else            
                erfenchazhao(str,chanum,left,mid-1);            
        }        
}
int main()
{
    int i,x,num;
    cin>>num;
    for(i=0;i<num;i++)
        cin>>a[i];
    cin>>x;
    sort(a,a+num);
    erfenchazhao(a,x,0,num-1);
    return 0;
}

二分查找(迭代实现)

首先是排好序的
int bsearch(int*A, int x, int y, int v)

{

        int m; while(x < y)

{  

         m = x+(y-x)/2;  

      if(A[m] == v) return m;  

     else if(A[m] > v)

     y = m;  

    else x = m+1;  

}

     return -1;

}

其实就是 lower_bound 函数 找第一个大于或等于它的 元素 的 下标。

 

经典样例二:全排列问题
有1,2,3,4个数,问你有多少种排列方法,输出来

仔细想想,采用分治的话,我们就要把大问题分解成很多的子问题,大问题是所有的排列方法那么我们分

解得到的小问题就是以1开头的排列,以2开头的排列,以3开头的排列,以4开头的排列现在这些问题有能

继续分解,比如以1开头的排列中,只确定了1的位置,没有确定2,3,4的位置,把23,4三个又看成大

问题继续分解,2做第二个,3做第二个,或者4做第二个一直分解下去,直到分解成的子问题只有一个数

字的时候,不再分解因为1个数字肯定只有一种排列方式啊,现在我们分解成了很多的小问题,解决一个

小问题就合并,合并成一个大点的问题,合并之后这个大点的问题也解决了,再将这些大点的问题合并成

一个更大的问题,那么这个更大点的问题也解决了,直到最大的问题解决为止

 

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#define pause system("pause")
using namespace std;
int k = 0;
char a[100];
int counta = 0;    //全排列个数的计数
void s(char a[], int i, int k)//将第i个字符和第k个字符交换
{
    char t = a[i];
    a[i] = a[k];
    a[k] = t;
}
void f(char a[], int k, int n)
{
    if (k == n - 1)        //深度控制,此时框里面只有一个字符了,所以只有一种情况,所以输出
    {
        puts(a);
        counta++;
    }
    int i;
    for (i = k; i < n; i++)
    {
        s(a, i, k);
        f(a, k + 1, n);
        s(a, i, k);//复原,就将交换后的序列除去第一个元素放入到下一次递归中去了,递归完成了再进行下一次循环。这是某一次循环程序所做的工作,这里有一个问题,那就是在进入到下一次循环时,序列是被改变了。可是,如果我们要假定第一位的所有可能性的话,那么,就必须是在建立在这些序列的初始状态一致的情况下,所以每次交换后,要还原,确保初始状态一致。
    }
}
int main()
{
    cin>>a;
    int l = strlen(a);//字符串长度
    f(a, k, l);
    printf("全排列个数:%d\n", counta);
    pause;
    return 0;
}

 

 


 

 

经典样例三:整数划分问题

#include<bits/stdc++.h>
using namespace std;
int n,total=0;
int a[10001]={1};
int print(int);
int search(int s,int t);                  //回溯算法
int main()
{
   cin>>n;
   search(n,1);        //将要拆分的数n传递给s
   cout<<total<<endl;       //这是总方案数
   return 0;
}
int search(int s,int t)
{
    int i;
    for(i=a[t-1];i<=s;i++)
    if(i<n)                //当前数i要大于等于前1位数,且不过n
    {
        a[t]=i;            //保存当前拆分的数i
        s-=i;              //s减去i,s的值继续拆分
        if(s==0) print(t);      //s==0  拆分结束输出结果
        else search(s,t+1);    //s>0,继续递归
       s+=i;                        //回溯:加上拆分的数,以便产分所有可能的拆分    *****《重要》  其实回溯这个算法  是人为的变动来 实现的,其实最初的递归 是完成下一步再返回,直到返回到最开始,就会停。
    }                                而回溯  就可能是 改变某些条件  例如这道题,将s=s+i  加入 。
    return 0;
}
int print(int t)
{
   total++;
   cout<<n<<"=";
   for(int i=1;i<=t;i++)
   {
       if(i==t)
       {
           cout<<a[i];
          cout<<endl;
       }
      else  cout<<a[i]<<"+";
   }
}

太难了 ,以下,不会做 。

 

 

 


经典样例4:归并排序

void merge_sort(int* A, int x, int y, int* T) 
{
    if (y - x > 1) 
    {
        int m = x + (y - x) / 2;
        int p = x, q = m, i = x;
        merge_sort(A, x, m, T);
        merge_sort(A, m, y, T);
        while (p < m || q < y) 
        {
            if (q >= y || (p < m && A[p] <= A[q])) 
            {
                T[i++] = A[p++];
            } else 
            {
                T[i++] = A[q++];
            }
        }
        for (int i = x; i < y; i++) 
        {
            A[i] = T[i];
        }
   }

经典样例五:棋盘覆盖问题

#include<stdio.h>
#define max 1024
int cb[max][max];//最大棋盘
int id=0;//覆盖标志位
int chessboard(int tr,int tc,int dr,int dc,int size)//tr,tc代表棋盘左上角的位置,dr ,dc代表棋盘不可覆盖点的位置,size是棋盘大小
{
    if(size==1)//如果递归到某个时候,棋盘大小为1,则结束递归
    {
        return 0;
    }
    int s=size/2;//使得新得到的棋盘为原来棋盘大小的四分之一
    int t=id++;
    if(dr<tr+s&&dc<tc+s)//如果不可覆盖点在左上角,就对这个棋盘左上角的四分之一重新进行棋盘覆盖
    {
        chessboard(tr,tc,dr,dc,s);
    }else//因为不可覆盖点不在左上角,所以我们要在左上角构造一个不可覆盖点
    {
        cb[tr+s-1][tc+s-1]=t;//构造完毕
        chessboard(tr,tc,tr+s-1,tc+s-1,s);//在我们构造完不可覆盖点之后,棋盘的左上角的四分之一又有了不可覆盖点,所以就对左上角棋盘的四分之一进行棋盘覆盖
    }

    if(dr<tr+s&&dc>=tc+s)//如果不可覆盖点在右上角,就对这个棋盘右上角的四分之一重新进行棋盘覆盖
    {
        chessboard(tr,tc+s,dr,dc,s);
    }else//因为不可覆盖点不在右上角,所以我们要在右上角构造一个不可覆盖点
    {
        cb[tr+s-1][tc+s]=t;
        chessboard(tr,tc+s,tr+s-1,tc+s,s);//在我们构造完不可覆盖点之后,棋盘的右上角的四分之一又有了不可覆盖点,所以就对右上角棋盘的四分之一进行棋盘覆盖
    }


     if(dr>=tr+s&&dc<tc+s)//如果不可覆盖点在左下角,就对这个棋盘左下角的四分之一重新进行棋盘覆盖
    {
        chessboard(tr+s,tc,dr,dc,s);
    }else//因为不可覆盖点不在左下角,所以我们要在左下角构造一个不可覆盖点
    {
        cb[tr+s][tc+s-1]=t;
        chessboard(tr+s,tc,tr+s,tc+s-1,s);//在我们构造完不可覆盖点之后,棋盘的左下角的四分之一又有了不可覆盖点,所以就对左下角棋盘的四分之一进行棋盘覆盖
    }

    if(dr>=tr+s&&dc>=tc+s)//如果不可覆盖点在右下角,就对这个棋盘右下角的四分之一重新进行棋盘覆盖
    {
        chessboard(tr+s,tc+s,dr,dc,s);
    }else//因为不可覆盖点不在右下角,所以我们要在右下角构造一个不可覆盖点
    {
        cb[tr+s][tc+s]=t;
        chessboard(tr+s,tc+s,tr+s,tc+s,s);//在我们构造完不可覆盖点之后,棋盘的右下角的四分之一又有了不可覆盖点,所以就对右下角棋盘的四分之一进行棋盘覆盖
    }

    //后面的四个步骤都跟第一个类似
}
int main()
{
    printf("请输入正方形棋盘的大小(行数):\n");
    int n;
    scanf("%d",&n);
    printf("请输入在%d*%d棋盘上不可覆盖点的位置:\n",n,n);
    int i,j,k,l;
    scanf("%d %d",&i,&j);
    printf("不可覆盖点位置输入完毕,不可覆盖点的值为-1\n");
    cb[i][j]=-1;
    chessboard(0,0,i,j,n);
    for(k=0;k<n;k++)
    {
        printf("%2d",cb[k][0]);
        for(l=1;l<n;l++)
        {
            printf(" %2d",cb[k][l]);
        }
        printf("\n");
    }
    return 0;
}
 

 

 

经典样例六:快速排序

void quick_sort(int* A, int p, int r)
 {
    if (p < r)
     {
        int q = partition(A, p, r);
        quick_sort(A, p, q - 1);
        quick_sort(A, q + 1, r);
    }
}
int partition(int* A, int p, int r) 
{
    int x = A[r];
    int i = p - 1;
    for (int j = p; j < r; j++)
     {
        if (A[j] < x)
         {
            i++;
            swap(A[i], A[j]);
        }
    }
    swap(A[i + 1], A[r]);
    return i + 1;
}

 

经样例七:求第k小/大元素

这是快排分区思想的应用,也要进行分区操作,和快排不同的是,快排分区之后还有继续处理基准元素两边的数据,而求k小/大不用,只用处理一边即可假如现在这里5个元素,分为1,2,3,4,5号位置第一种情况:假设求第3小元素,假设第一次分区的基准元素完成分区后在第2号位置,那么我们知道3>2所以只要对基准元素后面的元素继续分区就可以(注意k的值要变了,k代表的是在升序有序数组的1相对位置,现在对第一次分区的基准元素后面的元素进行分区操作,区间大小是变小了的,所以k值是要跟着变的)讲了这么多,所以分治的思想到底体现在哪里呢?跟快排一样,有分区操作,所以分治的思想在这里的体现和在快排的体现都是一样的,不同的是这里只要对基准元素前面元素或者后面元素进行继续分区(如果需要继续分区的话),而快排是基准元素两边都要继续分区的贴个代码(采用的是随机分区)
 

代码:

#include<bits/stdc++.h>
using namespace std;
void swap_t(int a[],int i,int j)
{
    int t=a[i];
    a[i]=a[j];
    a[j]=t;
}
int par(int a[],int p,int q)//p是轴,轴前面是比a[p]小的,后面是比a[p]大的
{
    int i=p,x=a[p];
    for(int j=p+1;j<=q;j++)
    {
        if(a[j]>=x)
        {
            i++;
            swap_t(a,i,j);
        }
    }
    swap_t(a,p,i);
    return i;//返回轴位置
}
int Random(int p,int q)//返回p,q之间的随机数
{
    return rand()%(q-p+1)+p;
}
int Randomizedpar(int a[],int p,int q)
{
    int i=Random(p,q);
    swap_t(a,p,i);//第一个和第i个交换,相当于有了一个随机基准元素
    return par(a,p,q);
}
int RandomizedSelect(int a[],int p,int r,int k)
{
    if(p==r)
        return a[p];
    int i=Randomizedpar(a,p,r);
    int j=i-p+1;
    printf("i=%d j=%d\n",i,j);
    if(k<=j)
        return RandomizedSelect(a,p,i,k);
    else
        return RandomizedSelect(a,i+1,r,k-j);
}
int main()
{
    int n;
    scanf("%d",&n);
    int a[n];
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
    }
    int x=RandomizedSelect(a,0,n-1,2);
    printf("%d\n",x);
}
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

      

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值