递归和分治策略
1、递归:直接或者间接调用自身的的算法。
全排列问题:
package recursion;
import java.util.LinkedList;
import java.util.List;
public class Sort {
public voidperm(List<Integer> list, int k,intn){
if(k==n){
for(inti=0;i<n;i++){
System.out.print(list.get(i)+" ");
}
System.out.println();
}
else{
for(intj=k;j<n;j++){
swap(list,k,j);//1交换
perm(list,k+1,n);
swap(list,k,j);//2交换,还原1交换后的位置
}
}
}
public voidswap(List<Integer> list,intm,intn){//交换
int temp;
temp=list.get(m);
list.set(m,list.get(n));
list.set(n,temp);
}
public static void main(String[] args){
List<Integer>list=newLinkedList<Integer>();
for(inti=1;i<=3;i++){
list.add(i);
}
new Sort().perm(list,0,3);
}
}
汉诺塔问题:
package recursion;
public class Hanoi {
public void hanoi(intn,chara,charb,charc){//a桩借助c桩将n个盘子移动到b
if(n>0){
hanoi(n-1,a,c,b);//a桩借助b桩将n-1个盘子移动到c
move(a,b);//第 n个盘子,从a桩移动到b桩
hanoi(n-1,c,b,a);//c桩借助a桩将n-1个盘子移动到b
}
}
public void move(chara,charb){
System.out.println(a+"--->"+b);
}
public static void main(String[] args){
new Hanoi().hanoi(3,'a', 'b', 'c');
}
}
2、 分治法
分治法的基本思想:将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同,递归的解这些子问题,然后将各个子问题的解合并得到原问题的解。
二分搜索技术:
基本思想:将n个元素分成个数大致相同的两部分,取中间元素与搜索元素比较,如果相等,则找到,停止算法;如果小于,左边(右边)继续搜索;如果大于,右边(左边)继续搜索。
//逆序
public int binarySearch(List<Integer> list,int x, int n) {
// TODO自动生成的方法存根
intleft=0;
intright=list.size()-1;
while(left<=right){
intmid=(left+right)/2;
if(x==list.get(mid))return mid;//找到x
else if(x<list.get(mid))left=mid+1;//可能在右边
else if(x>list.get(mid))right=mid-1;//可能在左边
}
return -1;//未找到
}
//顺序
public int binarySearch(List<Integer> list,intx,int n){ intleft=0;
int right=list.size()-1;
while(left<=right){
intmid=(left+right)/2;
if(x==list.get(mid))return mid;//找到x
else if(x<list.get(mid))right=mid-1;//可能在左边
else if(x>list.get(mid))left=mid+1;//可能在右边
}
return -1;//未找到
}
合并算法:
基本思想:将待排序元素分成大小大致相同的两个子集合,分别对这两个子集合进行排序,最终将排好序的子集合合并成所要求的排好序的集合。
public void mergeSort(List<Integer> list,intleft,intright){
if(left<right){
intmid=(left+right)/2;
mergeSort(list,left,mid);//左分解
mergeSort(list,mid+1,right);//右分解
List<Integer> listCopy=newLinkedList<Integer>();
for(inti=0;i<list.size();i++)
listCopy.add(-1);
merge(list,listCopy,left,mid,right);//合并
copy(list,listCopy,left,right);//赋值
}
}
public void merge(List<Integer> list,List<Integer>listCopy,intleft,intmid,intright){
int i=left,j=mid+1,k=left;
while(i<=mid&&j<=right){
if(list.get(i)<=list.get(j))listCopy.set(k++,list.get(i++));
elselistCopy.set(k++,list.get(j++));
}
if(i>mid)for(intq=j;q<=right;q++)listCopy.set(k++,list.get(q));
if(j>right)for(intq=i;q<=mid;q++)listCopy.set(k++,list.get(q));
}
public void copy(List<Integer> list,List<Integer>listCopy,intleft,intright){
for(inti=left;i<=right;i++)list.set(i,listCopy.get(i));
}
快速排序:
public voidquickSort(List<Integer> list,intleft,intright){
if(left<right){
intq=partition(list,left,right);
quickSort(list,left,q);//对左半段排序
quickSort(list,q+1,right);//对右半段排序
}
}
public int partition(List<Integer> list,intleft,intright){
int l=left,r=right;
int x=list.get(left);
while(true){
while(list.get(++l)<x&&l<r);
while(list.get(--r)>x&&l<r);
if(l>r)break;
if(l<=r) swap(list,l,r);
}
list.set(left,list.get(r));
list.set(r,x);
returnr;
}
动态规划
基本思想:将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。(每次保存子问题的答案,而在需要时再找到以求得的答案,这样就可以避免大量的重复计算)
与分治法不同的是:适合于动态规划求解的问题,静分解后得到的子问题往往不是互相独立的。若用分治法求解这类问题,则分解得到的子问题太多,以至于最后解决原问题需要耗费指数时间。
动态规划适用于解最优化问题,步骤:
1、找到最优解的性质,并刻画其结构特征。
2、递归地定义最优解。
3、以自底向上的方式计算出最优值。
4、根据计算最优值时的得到的信息,构造最优解。
动态规划算法的基本要素:
1、最优子结构性质:当问题的最优解包含了其子问题的最优解时,称该问题 具有最优子结构性质。
2、子问题重叠性质:在用递归法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次,动态规划正是利用这种子问题的重叠性质,对每个子问题只解一次,而后将其解保存到一个表格中,当再次需要解此问题时,只是简单的查看一下结果。
矩阵连乘问题:
1、分析最优解的结构
计算A[1:n]的最优次序所包含的计算矩阵子链A[1:k]和A[k+1:n]的次序也是最优的。假设有一个计算A[1:k]的次序需要的计算量更少,则用此次序替换原来的计算A[1:k]的次序,得到的计算A[1:n]的计算量将比最优次序所需的计算量更少,这是一个矛盾。所以原问题最优,子问题也最优。
2、 建立递归关系
0 i=j;
m[i][j]=
min{m[i][k]+m[k+1][n]+pi-1pkpj}
3、程序
0-1背包问题:
public void MatrixChain(intp[],intn,intm[][],ints[][]){
//p[i-1]*p[i]矩阵A[i]的行列值,n为矩阵个数,m[i][j]计算A[i:j]的最优值
//s[i][j]最优断开位置
for(inti=1;i<=n;i++)m[i][i]=0;
for(intr=2;r<=n;r++)
for(inti=1;i<=n-r+1;i++){
intj=i+r-1;
m[i][j]=m[i+1][j]+p[i-1]*p[i]*p[j];//初始化m[i][j]
s[i][j]=i;
for(intk=i+1;k<j;k++){
intt=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];//递归关系
if(t<m[i][j]){//赋予小值
m[i][j]=t;
s[i][j]=k;
}
}
}
}
public void knapsack(intw[],intv[],intn,intc,intm[][]){
int jMax=Math.min(w[1]-1, c);
for(inti=0;i<=c;i++){
if(i<=jMax)m[1][i]=0;//放不下第一个物品
elsem[1][i]=v[1];
}
for(inti=2;i<n;i++){
jMax=Math.min(w[i]-1,c);
for(intj=0;j<=c;j++){
if(j<=jMax)m[i][j]=m[i-1][j];//第i个物品放不下
else{
m[i][j]=Math.max(m[i-1][j],m[i-1][j-w[i]]+v[i]);//取较大者
}
}
m[n][c]=m[n-1][c];//对最后一个位置赋值
if(c>=w[n])m[n][c]=Math.max(m[n-1][c],m[n-1][c-w[n]]+v[n]);
}
}
public void traceBack(intm[][],intw[],intc,intn,intx[]){//求轨迹
for(inti=n;i>1;i--)
if(m[i][c]==m[i-1][c])x[i]=0;
else {x[i]=1;c-=w[i];}
x[1]=(m[1][c]>0)?1:0;
}
贪心算法
贪心算法不从整体最优上加以考虑,它所做的选择只是在某种意义的局部最优的选择。
贪心算法的基本要素:
1、贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
与动态规划的区别:在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择;在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解做出这个选择后产生的相应的子问题。动态规划以自底向上的方式,贪心算法以自顶向下方式。
2、最优子结构性质
背包问题:首先将物品单位价值从大到小的顺序排序,利用单位价值从大到小的顺序装入,如果物品能够完全装入,则直接装入;如果不能完全装入,则分割部分装入。
最优装载:首先将集装箱利用重量从小到大排序,采用最轻者先装的贪心策略。
3、单源最短路径:单个源点到连通图中任一顶点的最短路径
public void dijkstra(intc[][],intdist[],intn,intv,intprev[]){
Map<Integer,Boolean> map=new HashMap<Integer,Boolean>();
for(inti=1;i<=n;i++){//初始化
dist[i]=c[v][i];
map.put(i,false);
if(dist[i]==65535)prev[i]=0;//65535代表无穷大,即不可达
elseprev[i]=v;
}
dist[v]=0;map.put(v,true);//对源点初始化
for(inti=2;i<=n;i++){
inttemp=65535;
intu=v;
for(intj=1;j<=n;j++){//选择最小长度,贪心策略
if(dist[j]<temp&&!map.get(j)){
temp=dist[j];
u=j;
}
}
map.put(u,true);
for(intk=1;k<=n;k++){//重新更新dist[]
if(!map.get(k)&&c[u][k]<65535){
if(dist[k]>(dist[u]+c[u][k])) {
dist[k]=dist[u]+c[u][k];
prev[k]=u;
}
}
}
}
}
4、 Prim算法:从某一顶点开始,将该顶点加入到顶点集中,如果顶点集中顶点数不 小于顶点总数,寻找顶点集中顶点到不属于顶点集中的顶点的长度最小的边长,依次循环,直至结束。
public voidprim(intc[][],intv,intn,List<String> list){
Map<Integer,Boolean>map=new HashMap<Integer,Boolean>();//保存已包含节点
map.put(v,true);
int sum=0;
int start = 0,finish = 0;
while(map.size()<n){//依次取出包含节点集合
Set<Integer>set=map.keySet();
Iterator<Integer>it=set.iterator();
int min=65535;
int minPoint = 0;
while(it.hasNext()){
intnum=(Integer)it.next();
for(inti=1;i<=n;i++){
if(!map.containsKey(i)){
if(c[num][i]<min){//取最小,贪心策略
min=c[num][i];
start=num;
finish=i;
minPoint=i;
}
}
}
}
list.add(start+" "+finish+"="+min);
sum+=min;
map.put(minPoint,true);
}
list.add("最小生成树权值和:"+sum);
}
5、kruskal算法:将所有边加入一个集合中,每次去集合中边长最小的边,将该边加入到一个新的集合中,如果加入新的集合后,新集合中存在环,则将新加入的边从新集合中删去,如果不存在环,判断所加入的边长是否等于顶点总数-1,等于则结束,小于继续循环以上步骤。
public void kruskal(TreeSet<Edge>treeSet,intn,ArrayList<Edge> arrayList){
int i=0;
while(i<n-1){
Edgeedge=treeSet.first();//取出树集首元素即边长最小的元素
treeSet.remove(edge);//取出后删除树集首元素
arrayList.add(edge);//最小元素加入集合中
i++;
if(!verification(arrayList)){//判断是否有环
System.out.println("发现一个环");
arrayList.remove(i-1);//集合中已经存在环,删除新加入元素
i--;
}
}
}
public booleanverification(ArrayList<Edge> arrayListEdge){//判断是否存在循环,即循环的连通分量
Set<Integer>set=newHashSet<Integer>();
for(inti=0;i<arrayListEdge.size();i++){
Edge edge=arrayListEdge.get(i);
if(!set.isEmpty()){
/*如果新加入边的两个顶点都一定在已存在顶点集中,此时已判定的边数大于顶点树-1,判断此时有环*/ if(set.contains(edge.getPointI())&&set.contains(edge.getPointJ())&&set.size()-1<arrayListEdge.size())returnfalse;
}
set.add(edge.getPointI());
set.add(edge.getPointJ());
}
return true;
}
回溯法
回溯法:在问题的解空间树中,按深度优先策略,从根节点出发搜索解空间树,算法搜索到解空间树的任一结点时,先判断该结点是否包含问题的解,如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯,否则,进入该子树,继续按深度优先策略搜索。回溯法求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索一遍才结束。回溯法求问题的一个解时,只要搜索到问题的一个解就可结束。
回溯法的解空间树分成两类:
子集树:当所给的问题是从n个元素的集合S中找到满足某种性质的子集时,相应的解空间树即是子集树。
排序树:当所给的问题是确定n个元素满足某种性质的排序时,相应的解空间树称为排序树。
1、TSP:排序树
public void backTrack(intm[][],intt,intx[],intn){
//m[][]两点间的权值,x[]顶点集合,n顶点总数,t搜索深度
if(t>n){
if(sum<=bestSum){//保留最优路径和,及其路径
if(sum==bestSum){
set.add((ArrayList<Integer>)list.clone());
}
else{
set.clear();
bestSum=sum;
bestList=(ArrayList<Integer>)list.clone();
set.add(bestList);
}
}
//System.out.println(list+" ="+sum);
}
else{
for(inti=t;i<=n;i++){
swap(x,t,i);//交换
list.add(x[t]);
if(list.size()>1)sum+=m[list.get(list.size()-2)][list.get(list.size()-1)];
if(list.size()==n)sum+=m[list.get(list.size()-1)][list.get(0)];
if(sum<bestSum) backTrack(m,t+1,x,n);//递归 ,剪枝函数
if(list.size()==n)sum-=m[list.get(list.size()-1)][list.get(0)];
if(list.size()>1)sum-=m[list.get(list.size()-2)][list.get(list.size()-1)];
list.remove(list.size()-1);
swap(x,t,i);//还原
}
}
}
2、 N后问题:子集树
public void backTrack(intt,intn){
if(t>n){//出现可行解
sum++;
//System.out.println(list);
bestList.add((ArrayList<Integer>)list.clone());//所有可行的序列
}
else{
for(inti=1;i<=n;i++){
list.add(i);//新判断节点放入集合中
if(place(t)) {//判断新的位置是否可行
backTrack(t+1,n);//如可行进入下一行
list.remove(list.size()-1);//退一行
}
elselist.remove(list.size()-1);
}
}
}
public boolean place(intk){//判断第k个皇后的位置是否可行
for(intj=1;j<k;j++){
if(Math.abs(k-j)==Math.abs(list.get(k-1)-list.get(j-1))||list.get(j-1)==list.get(k-1))returnfalse;
}
return true;
}
3、 回溯法搜索子集树的一般算法:
void Backtrack(intt){
if(t>n) Output(x);
else
for(inti=0;i<n;i++){
x[t]=i;
if(Constraint(t)&&Bound(t))Backtrack(t+1);
}
}
回溯法搜索排列树的一般算法:
void Backtrack(intt){
if(t>n) Output(x);
else
for(inti=t;i<n;i++){
swap(x[t],x[i]);
if(Constraint(t)&&Bound(t))Backtrack(t+1);
swap(x[t],x[i]);
}
}
分支限界法
分支限界法类似于回溯法,也是问题的解空间上搜索问题解的算法。一般分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出解空间中满足约束条件的所有解,而分支限界法的求解目标是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数达到极大或者极小的解,即在某种意义上的最优解。
分支限界法是以广度优先或者最小耗费优先的方式搜索解空间,搜索策略为:
在扩展结点处,先生成其所有儿子结点,然后再从当前的活结点表中选择下一个扩展节点,为了有效地选择一个扩展节点,加速搜索的进程,在每一活结点处,计算一个函数值即限界,并根据函数值,从当前活结点表中选择一个最优的节点作为扩展节点,使搜索朝着解空间上有最优解的分支推进,以便尽快找出一个最优解。
分支限界法按照从活结点表中选择下一扩展节点的方式可分为:
1、队列式分支限界法:将活结点表组织成一个队列,并按队列的先进先出原则选择下一个结点为当前扩展结点。
public voidbranchBound(intm[][],intt,intn,intdist[],intprev[]){
Queue<Integer>queue=newLinkedList<Integer>();
ArrayList<Integer>set=newArrayList<Integer>();
queue.add(t);
dist[t]=0;
prev[t]=t;
while(!queue.isEmpty()){
int v=queue.poll();
set.add(v);
if(v==t){
for(inti=1;i<=n;i++){
if(i!=v){
dist[i]=m[v][i];
prev[i]=v;
queue.add(i);
}
}
}
else{
for(inti=1;i<=n;i++){
if(m[v][i]<65535){
if(v!=i&&dist[v]>dist[i]+m[v][i]) {
dist[v]=dist[i]+m[v][i];
prev[v]=i;
}
if(!set.contains(i)&&!queue.contains(i)) {
queue.add(i);
prev[v]=i;
}
}
}
}
}
}
2、优先队列式分支限界法:将活结点表组织成一个优先队列,并按优先队列中规定的结点优先级选择优先级最高的下一个结点成为当前扩展结点。
版权声明:本文为博主原创文章,未经博主允许不得转载。