分治 基本思想
分治:分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了。
但是我们应该在什么时候用分治呢?做题的时候就不知道用什么算法,能用分治法的基本特征:
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);
}