傳統算法-知識總結-遞推+遞歸+分治+動態規劃+貪心算法+回溯算法+分支
目錄
一:算法基礎
1.1.算法基礎介紹
1.1.1 算法滿足4條性質:
1)輸入:有0個或者多個由外部提供的的量作爲算法的輸入
2)輸出:算法產生至少一個量作爲輸出
3)確定性:組成算法的每條指令是清晰的和無歧義的
4)有限性:算法中每條指令的執行次數是有限的,執行每條指令的時間也是有限的。
1.1.2 算法定義:
1.1.3 算法設計過程
1.2.歐幾里德算法介紹
1.2.1 定義:
歐幾里德算法也被稱爲輾轉相除法,用於計算兩個整數a, b的最大公因子。符號記爲:gcd(a,b).特別gcd(0, n) = 0,因爲任何整數都能整除0;
1.2.2 算法內容:
通俗的講:假設a比b大,我們要找a, b的最大公因子。第一步,用a除以b餘數爲y.第二步:把第一步中的除數當作被除數,把第一步中的餘數當作除數再進行相除;第三步:重複第二步的步驟,一直到餘數爲0,結束算法,此時剩餘的最後的被除數就是我們要找的最大公因子。
1.2.3 算法證明:
對於任何可以整除a和b的整數,那麼它也一定可以整除(a-b)和 b.選擇該整數爲gcd(a, b);同理,任何可以整除a-b和b的整數,一定可以整除a和b,因此我們選擇該整數爲gcd(a-b,b);由此可得:gcd(a,b)=gcd(a-b,b)。
因爲總有整數n,使得 a - n*b = a mod b,所以迭代可知:gcd(a-b,b)=gcd(a-2b,b)=...=gcd(a-n*b,b)=gcd(a mod b,b)。
二.遞推
2.1 遞推的定義
一個問題的求解需一系列的計算,在已知條件和所求問題之間總存在着某種相互聯繫的關係,在計算時,如果可以找到前後過程之間的數量關係(即遞推式),那麼,從問題出發逐步推到已知條件,此種方法叫逆推。無論順推還是逆推,其關鍵是要找到遞推式。
遞推算法的首要問題是得到相鄰的數據項間的關係(即遞推關係)。遞推算法避開了求通項公式的麻煩,把一個複雜的問題的求解,分解成了連續的若干步簡單運算。一般說來,可以將遞推算法看成是一種特殊的迭代算法
2.2 迭代法求解遞推方程
不斷用遞推方程的右部替換左部,每次替換,隨着 n 的降低在和式中多出一項,直到出現初值停止迭代,將初值代入並對和式求和
可用數學歸納法驗證解的正確性
2.3 遞推樣例
2.3.1 一元多項式求導
(1)題目
(2)c語言求解
(3)Java語言求解
注意四個細節
- 第一個坑:數字之間可能有多個空格 如果你是用Java切割字符串的話
- 第二個坑:當係數項是0的時候輸出0 0 *如:3 4 -5 2 6 1 0 1 對應輸出是12 3 -10 1 6 0 0 但是題目給出的輸出是 12 3 -10 1 6 0
- 第三個坑:當係數項不是0,指數是0的時候 什麼也不輸出 *如:3 4 -5 2 6 1 -2 0 對應輸出是12 3 -10 1 6 0 (-2 0沒對應的數字輸出)
- 第四個坑:當輸出多項式是空串的時候要輸出0 0 *如:輸入只有 -2 0 的時候 輸出空串 但是此時必須輸出0 0
代碼實現:
package temp;
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String arg[]) {
System.out.println("請輸入整數: ");
int n,fac;
Scanner scan = new Scanner(System.in);
String str = scan.nextLine();
String[] newstr =str.split("\\s+");
ArrayList<Integer>alist = new ArrayList<Integer>();
for(int i=0;i<newstr.length;i=i+2) {
int j = i+1;
if(Integer.valueOf(newstr[i])==0){
alist.add(0);
alist.add(0);
}
if(Integer.valueOf(newstr[i])!=0&&Integer.valueOf(newstr[j])==0) {
}
if(Integer.valueOf(newstr[i])!=0&&Integer.valueOf(newstr[j])!=0) {
alist.add(Integer.valueOf(newstr[i])* Integer.valueOf(newstr[j]));
alist.add(Integer.valueOf(newstr[j])-1);
}
}
if(alist.isEmpty()){ //如果將要輸出的是空字符串,那麼就輸出0 0
System.out.println("0 0");
}else{
for(int i=0 ;i<alist.size() ;i++){
System.out.print(alist.get(i));
if(i!=alist.size()-1){
System.out.print(" "); //行末不能有空格 控制空格的輸出
}
}
}
System.out.println();
}
}
2.3.2分魚程序
題目:
A、B、C、D、E五個人在某天夜裏合夥去捕魚,到第二天凌晨時都疲憊不堪,於是各自找地方睡覺。日上三杆,A第一個醒來,他將魚分爲五份,把多餘的一條魚扔掉,拿走自己的一份。B第二個醒來,也將魚分爲五份,把多餘的一條魚扔掉,保持走自己的一份。C、D、E依次醒來,也按同樣的方法拿走魚。問他們合夥至少捕了多少條魚?
這個程序非常巧妙,先給a[0]一個初值爲6,然後利用6去計算其他的魚,在進行判斷比較。
package temp;
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String arg[]) {
int countsum = 6;
for(int i=6; ;i++)
{
int count2 =(i-1)/5*4;
int count3 = (count2-1)/5*4;
int count4 = (count3-1)/5*4;
int count5 = (count4-1)/5*4;
if(i%5==1&&count2%5==1&&count3%5==1&&count4%5==1&&count5%5==1) {
System.out.println(i);
break;
}
}
}
}
三:遞歸
3.1 遞歸:
直接或者間接地調用自身的算法成爲遞歸算法。用函數自身給出定義的函數稱爲遞歸函數。
3.2 實例:
3.2.1 階乘
(1)階乘函數可遞歸定義爲:
(2)代碼實現
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String arg[]) {
System.out.println("請輸入整數: ");
int n,fac;
Scanner scan = new Scanner(System.in);
n= scan.nextInt();
//int []Result_Seq=new int[n];
fac = factorial(n);
System.out.println(fac);
}
public static int factorial(int m) {
if(m==0) return 1;
else return m*factorial(m-1);
}
}
3.2.2 Fibonacci數列:
(1)定義:
無窮數列1,1,2,3,5,8,13,21,34,55,........,稱爲Fibonacci數列,表現形式:
(2)代碼實現
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String arg[]) {
System.out.println("請輸入整數: ");
int n,fac;
Scanner scan = new Scanner(System.in);
n= scan.nextInt();
//int []Result_Seq=new int[n];
fac = fibonacci(n);
System.out.println(fac);
}
public static int fibonacci(int m) {
if(m<=1) return 1;
else return fibonacci(m-1)+fibonacci(m-2);
}
}
3.3 經典hanoi塔問題
3.3.1 問題定義:
漢諾塔是根據一個傳說形成的一個問題。漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着 64 片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。
我們將 Hanoi 問題抽象爲一種數學問題。首先給出如下三個柱子 A、B、C,其中 A 柱子上有從上到下從小疊到大的 N 個雲盤。現要求將A柱子上的圓盤都移動到 C 柱子上,其中,每次移動都必須滿足:
每次只能移動一個圓盤
小圓盤上不能放大圓盤
那麼針對這個數學問題,就可以提出相關問題:
移動 N 個圓盤最少需要多少次
第 M 步移動的是哪個圓盤以及圓盤移動方向
解題:設總共有 N 個圓盤,Steps表示總移動次數
3.3.2 解題思路
3.3.3Java代碼實現hanoi塔
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
static long s = 0;
public static void main(String args[]) {
System.out.println("開始hanoi塔遊戲 ");
int n = 0;
Scanner console = new Scanner(System.in);
n = console.nextInt();
System.out.println("漢諾塔層數爲" + n);
System.out.println("移動方案爲:");
hanoi(n, 'a', 'b', 'c');
System.out.println("需要移動次數:" + s);
}
static void hanoi(int n, char a, char b, char c) { //a爲初始塔,b爲目標塔,c爲輔助塔
if (n == 1){
System.out.println("n=" + n + " " + a + "-->" + c);
s++;
}
else{
/*遞歸的調用*/
hanoi(n-1,a,b,c); // 把 a移動到c藉助b;
System.out.println("n=" + n + " " + a + "-->" + c);
hanoi(n-1,b,a,c); //把b 移動到a藉助c.
s++;
}
}
}
四:分治
4.1 分治的基本概念
4.1.1.基本介紹
分治法的基本思想是將一個規模爲n的問題分解爲k個規模較小的子問題,這些子問題互相獨立且與原問題相同。遞歸地解決這些子問題,然後將各個子問題的解合併得到原問題的解。
4.1.2 算法的一般設計模式
divide-and-conquer(P){
if(|P| <= n0){
divide P into smaller subinstances P1, P2,.....,Pk;
for(i = 1; i<= k; i++){
yi = divide-and-conquer(Pi);
}
return merge(y1 , y2, ....., yk);
}
}
從分治算法的一般設計模式可以看出,用它設計出的程序一般是遞歸算法。
4.2 二分搜索技術
4.2.1 Java中封裝的二分搜索
以Java爲例,二分搜索已經被封裝了,例如,我們寫代碼的時候可以直接使用語句:
int pos = currentItemset.binarySearch(item); binarySearch()方法,若搜索鍵包含在列表中,則返回鍵的索引,否則,返回-1;
4.2.2二分搜索描述和思想
(1)問題描述:
給定已經排好序的n個元素a[ 0 : n-1],現在要在這n個元素中找出一特定元素x.
(2)核心思想:
二分搜索方法的基本思想是將n個元素分成個數大致相同的的兩半,取a[n / 2]與x作比較,如果x = a[n / 2],說明找到x,算法終止。如果x < a[n / 2],則只需要在數組a的左半部分繼續搜索x; 如果x > a[n / 2],則只需要在a的右半部分繼續搜索x.
4.2.3 代碼實現:
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String args[]) {
System.out.println("請輸入一行從小到大排序好的整數: ");
Scanner scan = new Scanner(System.in);
String str = scan.nextLine();
System.out.println("請輸入要尋找的數 ");
int x =scan.nextInt();
String[] newstr = str.split("\\s+");
ArrayList<Integer> a = new ArrayList<Integer>();
for(int i=0; i<newstr.length; i++) {
if(Integer.valueOf(newstr[i]) != ' ') {
a.add(Integer.valueOf(newstr[i]));
}
}
//System.out.println(x);
int start = 0;
int end = a.size();
int flag = binarysearch(a,x,start,end);
if(flag == 1) {
System.out.println("搜索成功");
}else {
System.out.println("搜索失敗");
}
}
static int binarysearch(ArrayList<Integer> a, int x,int start, int end) {
int flag = 0;
while(start<= end) {
int middle = (start+end)/2;
if(a.indexOf(middle) == x) {
flag =1;
System.out.println("找到了:" + a.indexOf(middle));
break;
}
else if(x< a.indexOf(middle)) {
end = middle -1;
binarysearch(a, x, start, end);
}
else if(x > a.indexOf(middle)) {
start = middle+1;
binarysearch(a,x,start,end);
}
}
return flag;
}
}
4.3 快速排序
4.3.1 快排的定義和思想:
快速排序算法是基於分治策略的一個排序算法。
假設我們現在對“6 1 2 7 9 3 4 5 10 8”這個 10 個數進行排序。首先在這個序列中隨便找一個數作爲基準數(不要被這個名詞嚇到了,就是一個用來參照的數,待會你就知道它用來做啥的了)。爲了方便,就讓第一個數 6 作爲基準數吧。接下來,需要將這個序列中所有比基準數大的數放在 6 的右邊,比基準數小的數放在 6 的左邊,類似下面這種排列。 3 1 2 5 4 6 9 7 10 8
核心思想步驟爲:分解->遞歸->合併
4.3.2 圖解快排過程
4.3.3 代碼實現
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String args[]) {
int[] a = new int[]{2,7,4,5,10,1,9,3,8,6};
int[] b = new int[]{1,2,3,4,5,6,7,8,9,10};
int[] c = new int[]{10,9,8,7,6,5,4,3,2,1};
int[] d = new int[]{1,10,2,9,3,2,4,7,5,6};
sort(a, 0, a.length-1);
System.out.println("排序後的結果:");
for(int x : a){
System.out.print(x+" ");
}
}
public static int divide(int a[],int start , int end) {
int base = a[end]; //每次以最右邊的元素爲基準值
while(start < end) {
while(start < end && a[start] <= base)
//從左邊開始遍歷,如果目前值比基準值小,就繼續往右走
start++;
if(start < end) {
int temp = a[start];
a[start] = a[end];
a[end] = temp;
end--; // 交換後,此時那個被調換的值也同時調到了正確的位置()
}
while(start < end && a[end] >= base)
end--; // 從右邊開始遍歷
if(start < end) {
int temp = a[start];
a[start] = a[end];
a[end] = temp;
start++;
}
}
return end; //return start 也行
}
public static void sort (int[] a , int start , int end) {
if(start > end ) {
return ; // 如果只有一個元素,則不需要排序
}
else {
int partition = divide(a,start,end);
sort(a, start , partition-1);
sort(a, partition+1, end);
}
}
}
五:貪心算法
5.1 貪心算法基本概念
5.1.1 定義
在問題求解時,總是做出當前看來是最好的選擇。也就是說不從整體最
優上加以考慮,所做出的僅僅是某種意義上的局部最優解。所以對所採用的貪心策略一定要仔細分析其是否滿足無後效性。
5.1.2 貪心算法的基本思路
(1)建立數學模型描述問題;
(2)把求解的問題分解成若干子問題;
(3)對每一個子問題求解,得到子問題的局部最優解;
(4)把子問題的局部最優解合成原來解問題的一個解
5.1.3 貪心算法適用問題
貪心策略適用的前提就是局部最優解能夠產生全局最優解。一般,對一個問題分析是否適用於貪心算法,可以先選擇該問題下的幾個實際數據進行分析,就可做出判斷
5.2 揹包問題
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String[] args) {
// 單位重量價值分別爲:10 5 7 6 3 8 90 100
double w[] = { 0, 50, 80, 30, 40, 20, 60, 10 ,1};//物體的重量
double v[] = { 0, 500, 400, 210, 240, 60, 480, 900,100 };//物體的價值
double M = 170;// 揹包所能容納的重量
int n = w.length - 1;// 物體的個數
double[] x = new double[n + 1];// 每個物體裝進的比例,大於等於0並且小於等於1
packages(w, v, M, n, x);//調用貪心算法函數
System.out.println("排序後的物體的重量:");
for(int i=1;i<=n;i++){
System.out.print(w[i]+"\t");
}
System.out.println();
System.out.println("排序後的物體的價值:");
for(int i=1;i<=n;i++){
System.out.print(v[i]+"\t");
}
double[]t=new double[n+1];//定義一個數組表示單位重量物體的價值
for(int i=1;i<=n;i++){
t[i]=v[i]/w[i];
}
//用冒泡排序對double[]t進行排序(大的在前)
for(int i=1;i<=n;i++){
for(int j=1;j<=n-i;j++){
if(t[j]<t[j+1]){
double temp=t[j];
t[j]=t[j+1];
t[j+1]=temp;
}
}
}
System.out.println();
System.out.println("排好序後的單位物體的價值: ");
for(int i=1;i<=n;i++){
System.out.print(t[i]+"\t");
}
double maxValueSum=0; //用來存放揹包能裝下的物體的最大價值總和
for(int i=1;i<x.length;i++){
maxValueSum+=x[i]*v[i];
}
System.out.println();
System.out.println("排序後每個物體裝進揹包的比例:");
for(int i=1;i<=n;i++){
System.out.print(x[i]+"\t");
}
System.out.println();
System.out.println("揹包能裝下的物體的最大價值總和爲: "+maxValueSum);
}
public static void packages(double[] w, double[] v, double M, int n, double[] present) {
unitvaluesort(w,v,n);
double restcapacity = M ; // 揹包剩餘的容量
int i; //表示第幾個物品
for(i = 1; i<= n; i++) {
if(w[i] <= restcapacity) {
present[i] =1;
restcapacity = restcapacity - w[i];
}
else {
break;
}
}
if(i<= n) {
present[i] = restcapacity/w[i];
}
}
public static void unitvaluesort(double[] w, double[] v, int n) {
double[] unitvalue = new double[n+1];
for(int i=1;i<=n;i++) {
unitvalue[i] = v[i]/w[i];
}
// 以單位重量的價值的大小進行排序,同時以此爲依據
for(int i =1; i<= n; i++) {
for(int j=1; j<=n-1; j++) {
if(unitvalue[j]< unitvalue[j+1]) {
double temp = unitvalue[j];
unitvalue[j] = unitvalue[j+1];
unitvalue[j+1] = temp;
double temp2=w[j];
w[j]=w[j+1];
w[j+1]=temp2;
double temp3=v[j];
v[j]=v[j+1];
v[j+1]=temp3;
}
}
}
}
}
5.3 哈夫曼編碼
5.4 單源最短路徑之dijkstra
5.5 最小生成樹之prim
5.6 最小生成樹之kruskal
(5.3節到5.6節暫時不寫博客,後期有空閒時間再補上)
六:動態規劃
6.1 動態規劃引例-鈔票使用問題
6.1.1 問題描述
假設您是個土豪,身上帶了足夠的1、5、10、20、50、100元面值的鈔票。現在您的目標是湊出某個金額w,需要用到儘量少的鈔票。依據生活經驗,我們顯然可以採取這樣的策略:能用100的就儘量用100的,否則儘量用50的……依次類推。在這種策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10張鈔票。這種策略稱爲“貪心”:假設我們面對的局面是“需要湊出w”,貪心策略會儘快讓w變得更小。能讓w少100就儘量讓它少100,這樣我們接下來面對的局面就是湊出w-100。長期的生活經驗表明,貪心策略是正確的。但是,如果我們換一組鈔票的面值,貪心策略就也許不成立了。如果一個奇葩國家的鈔票面額分別是1、5、11,那麼我們在湊出15的時候,貪心策略會出錯:
15=1×11+4×1 (貪心策略使用了5張鈔票)
15=3×5 (正確的策略,只用3張鈔票)
爲什麼會這樣呢?貪心策略錯在了哪裏?鼠目寸光。
剛剛已經說過,貪心策略的綱領是:“儘量使接下來面對的w更小”。這樣,貪心策略在w=15的局面時,會優先使用11來把w降到4;但是在這個問題中,湊出4的代價是很高的,必須使用4×1。如果使用了5,w會降爲10,雖然沒有4那麼小,但是湊出10只需要兩張5元。在這裏我們發現,貪心是一種只考慮眼前情況的策略。
6.1.2 如何避免不是最優解
如果直接暴力枚舉湊出w的方案,明顯複雜度過高。太多種方法可以湊出w了,枚舉它們的時間是不可承受的。我們現在來嘗試找一下性質。重新分析剛剛的例子。w=15時,我們如果取11,接下來就面對w=4的情況;如果取5,則接下來面對w=10的情況。我們發現這些問題都有相同的形式:“給定w,湊出w所用的最少鈔票是多少張?”接下來,我們用f(n)來表示“湊出n所需的最少鈔票數量”。那麼,如果我們取了11,最後的代價(用掉的鈔票總數)是多少呢?明顯cost =f(4)+1=4+1=5 ,它的意義是:利用11來湊出15,付出的代價等於f(4)加上自己這一張鈔票。現在我們暫時不管f(4)怎麼求出來。
依次類推,馬上可以知道:如果我們用5來湊出15,cost就是 f(10)+1 = 2+1 =3。
那麼,現在w=15的時候,我們該取那種鈔票呢?當然是各種方案中,cost值最低的那一個!
這個式子是非常激動人心的。我們要求出f(n),只需要求出幾個更小的f值;既然如此,我們從小到大把所有的f(i)求出來不就好了?注意一下邊界情況即可。
6.1.3 鈔票問題代碼
import java.util.ArrayList;
import java.util.Scanner;
public class algorithmtest001 {
public static void main(String[] args) {
int n=15;
int[] f = new int[16];
f[0] = 0;
for(int i=1; i<= n; i++) {
int cost = 100;
if(i>=11) {
cost = Math.min(cost , f[i-11]+1);
}
if(i>=5) {
cost = Math.min(cost , f[i-5]+1);
}
if(i>=1) {
cost = Math.min(cost , f[i-1]+1);
}
f[i]= cost;
System.out.println("請輸出此時的整數n對應的次數:: "+ i+ " : "+cost);
}
}
}
運行結果:
6.2 基本概念:
6.2.1 動態規劃的定義:
動態規劃與分治法類似,其基本思想也是將待求問題分解成若干子問題,先求解子問題,然後從這些子問題的解得到原問題的解。與分治法不同的是,適合與用動態規劃求解的問題,經分解得到的子問題往往不是互相獨立的,若使用分治法來解決這類問題,則分解得到的子問題數目太多,以至於最後解決原問題需要耗費指數時間,然而,不同子問題的數目常常只有多項式量級。在用分治法求解時,有些子問題被重複計算了許多次。如果能夠保存已經解決子問題的答案,在需要的時候再找出這些已經求得的答案,這樣就可以避免大量的重複運算,從而得到多項式時間算法。
爲了達到上述目的,可以使用一個表來記錄所有已經解決子問題的答案。不管子問題以後是否被用到,只要它被計算過,就將結果填入表中。這就是動態規劃的基本思想。
6.2.2 無後效性
如果給定某一階段的狀態,則在這一階段以後過程的發展不受這階段以前各段狀態的影響。即未來與過去無關。例如,
一旦f(n)確定,“我們如何湊出f(n)”就再也用不着了。要求出f(15),只需要知道f(14),f(10),f(4)的值,而f(14),f(10),f(4)是如何算出來的,對之後的問題沒有影響。
6.2.3 最優子結構
回顧我們對f(n)的定義:我們記“湊出n所需的最少鈔票數量”爲f(n)。f(n)的定義就已經蘊含了“最優”。利用w=14,10,4的最優解,我們即可算出w=15的最優
大問題最優解可以由小問題的最優解推出。
6.3 動態規劃樣例:最長公共子系列
6.3.1 最長子問題描述:
LCS問題陳述:給定兩個序列,找出兩個序列中存在的最長子序列的長度。子序列是以相同的相對順序出現的序列,但不一定是連續的。例如,“abc”、“abg”、“bdf”、“aeg”、“'”acefg“等等都是“abcdefg”的子序列。所以一個長度爲n的字符串有2^n個不同的可能子序列。具體例子:對於給定的字符串 “ABCDGH” 和 “AEDFHR”,其最長公共子序列爲: “ADH”,最長公共子序列的長度爲:3。對於給定的字符串 “AGGTAB” 和 “GXTXAYB”,其最長公共子序列爲:“GTAB”,最長公共子序列的長度爲:4。
6.3.2 最長子問題分析
6.3.3 最長子問題解決
由上面的分析可以看到,問題具有遞歸結構。因此可以使用遞歸算法。但是因爲該問題具有重疊子結構性質,爲了避免有些子問題中同一個子問題被重複計算,我們使用動態規劃算法實現。
代碼實現
import java.util.ArrayList;
import java.util.Scanner;
public class temp {
public static void main(String[] args) {
//algorithmtest001 lcs = new algorithmtest001();
String s1 = "AGGTAB";
String s2 = "GXTXAYB";
char[] X = s1.toCharArray();
char[] Y = s2.toCharArray();
int m = X.length;
int n = Y.length;
System.out.println("Length of LCS is "+ " "+ lcs(X,Y,m,n));
}
static int lcs(char[] X, char[] Y, int m, int n) {
int L[][] = new int[m+1][n+1];
for(int i=0; i<= m; i++) {
for(int j=0; j<= n; j++) {
if(i == 0 || j==0) {
L[i][j] = 0;
}
else if(X[i -1] == Y[j-1]) {
L[i][j] = L[i-1][j-1] +1;
}
else {
L[i][j] = Math.max(L[i-1][j], L[i][j-1]);
}
}
}
return L[m][n];
}
}
七:回溯法
7.1 回溯法的基本定義和思想
回溯法有“通用的解題法”之稱。用回溯法可以系統地搜索一個問題的所有解或者任意解。回溯法是一個既帶有系統性又帶有跳躍性的搜索算法。它在問題的解空間樹中按照深度優先策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任一節點時候,先判斷該節點是否包含問題的解。如果肯定不包含,則跳過對以該節點爲跟的子樹的搜索,逐層向其祖先節點回溯。否則進入該子樹,繼續按照深度優先策略搜索。回溯法求解問題所有解時,只要搜索到問題的一個解就可以結束。
7.2 回溯法的解題步驟
(1)針對所給問題,確定問題的解空間;(首先明確定義問題的解空間,問題的解空間至少包括包含問題的一個解);
(2)確定節點擴展搜索規則;
(3)以深度優先搜索方式搜索解空間,並且在搜索過程中使用剪枝函數避免無效搜索
7.3 回溯法代碼框架
7.3.1 遞歸回溯框架
回溯法是對解空間的深度優先搜索,在一般情況下使用遞歸函數來實現回溯法比較簡單,其中i爲搜索的深度,框架如下f
void backtrack (int t) //t表示遞歸深度
{
if (t>n) output(x); //n表示深度界限
else
for (int i=f(n,t);i<=g(n,t);i++) // f(n,t),g(n,t)分別表示當前擴展結點未搜索過的子樹的起始編號和終止編號
{
x[t]=h(i);
if (constraint(t)&&bound(t)) //滿足約束函數和限界函數
backtrack(t+1);
}
}
7.3.2 非遞歸的算法框架
void iterativeBacktrack ()
{
int t=1;
while (t>0) {
if (f(n,t)<=g(n,t))
for (int i=f(n,t);i<=g(n,t);i++) {
x[t]=h(i);
if (constraint(t)&&bound(t)) {
if (solution(t)) output(x);
else t++;}
}
else t--;
}
}
7.4 n皇后問題
7.4.1 題目描述
在n ~n格的棋盤上放置彼此不受攻擊的n個皇后按照國際象棋的規則,皇后可以攻擊與之處在同一行或者同一列或者同一斜線上的棋子。n皇后問題等價於在n ~n格的棋盤上放置n個皇后,任何兩個皇后不放在同一行或者同一列或者同一斜線上。
74.2.代碼實現
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
public class QueenSolution {
private int[][] board = new int[8][8];
private int total = 0; //結果的數量
public void putQueen(int k) {
int max = board.length;
//放置最後一個的時候,說明棋盤放置完畢,輸出結果
if(k>= max) {
total++;
System.out.println(String.format("==========%s=======", total));
for(int i =0; i< max;i++) {
System.out.println(Arrays.toString(board[i]));
}
System.out.println("==================");
}
else {
for(int i =0; i< max; i++) {
if(check(k,i)) {
board[k][i] =1;
putQueen(k+1);
board[k][i] = 0;
}
}
}
}
private boolean check(int row, int col) {
// 檢查列是否有皇后
for(int i =0; i< row; i++) {
if(board[i][col] == 1) {
return false;
}
}
//檢查左上對角線
for(int m = row-1, n= col-1;m>=0&& n>=0;m--,n--) {
if(board[m][n] == 1) {
return false;
}
}
//檢查右上對角線是否有皇后
for (int m = row - 1, n = col + 1; m >= 0 && n < board[0].length; m--, n++) {
if (board[m][n] == 1) {
return false;
}
}
return true;
}
public static void main(String[] args) {
QueenSolution solution = new QueenSolution();
solution.putQueen(0);
}
}
八:分支限界
8.1.基本介紹
分支限界法類似於回溯法,也是在問題的解空間上搜索問題解的算法。一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出解空間中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函數值達到極大或者極小的解,即在某種意義下的最優解。
分支限界法通常以廣度優先搜索或以最小耗費優先的方式搜索問題的解空間樹。問題的解空間樹是表示問題解空間的一顆有序樹,常見的有子集樹和排列樹。在搜索問題的解空間樹時,分支限界法與回溯法的主要區別在於他們對當前擴展節點所採用的擴展方式不同,在分支限界法中,每一個活結點只有一次機會成爲擴展節點。活節點一旦成爲擴展節點,就一次性產生其所有兒子節點。在這些兒子節點中,導致不可行解或者導致非最優解的兒子節點被捨棄,其餘兒子節點被加入活節點表中。此後,從活節點表中取下一節點成爲當前擴展節點,並且重複上述節點的擴展過程。這個過程一直持續到找到所需的解或者活節點表爲空時爲止。
8.2 分支限界法與回溯法的區別
- 回溯法
1)(求解目標)回溯法的求解目標是找出解空間中滿足約束條件的一個解或所有解。
2)(搜索方式深度優先)回溯法會搜索整個解空間,當不滿條件時,丟棄,繼續搜索下一個兒子結點,如果所有兒子結點都不滿足,向上回溯到它的父節點。
- 分支限界法
1)(求解目標)分支限界法的目標一般是在滿足約束條件的解中找出在某種意義下的最優解,也有找出滿足約束條件的一個解。
2)(搜索方式)分支限界法以廣度優先或以最小損耗優先的方式搜索解空間。
- 常見的兩種分支界限法
a.隊列式(FIFO)分支界限法(廣度優先):按照隊列先進先出原則選取下一個結點爲擴展結點
b.優先隊列式分支限界法(最小損耗優先):按照優先隊列規定的優先級選取優先級最高的結點成爲當前擴展結
8.3 分支限界--單源最短路徑問題
8.3.1 單源最短路徑問題介紹
問題描述:給定帶權重的有向圖G=(V, E),圖中的每一條邊都具有非負的長度,求從源頂點s到目標頂點t的最短路徑問題。
8.3.2 單源最短路徑問題思路
(1) 把源頂點s作爲根節點開始進行搜索。對源頂點s的所有鄰接頂點,都產生一個分支結點,估計從源點s經該鄰接頂點到達目標頂點t的距離作爲該分支結點的下界。選擇下界最小的分支結點,對該分支結點所對應的頂點的所有鄰接頂點繼續進行上述的搜索.
(2)下界估算方法:(貪心算法思想)
(注意:分支限定算法思想中也隱藏着貪心算法和回溯算法的基本思維)
(3)具體樣例說明:
8.3.3 單源最短路徑問題代碼實現
輸入:
5
-1 10 -1 30 100
-1 -1 50 -1 -1
-1 -1 -1 -1 10
-1 -1 20 -1 60
-1 -1 -1 -1 -1
package temp;
import java.io.BufferedInputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Scanner;
/**
* 分支限界法解決單源最短路徑
*/
public class BBShortestPath {
public static void main(String[] args) {
System.out.println("請輸入節點個數和節點之間路徑長度");
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n = cin.nextInt();
int[] dist = new int[n+1];
int[] pre = new int[n+1];
int[][] map = new int[n+1][n+1];
for(int i= 1;i<=n;i++) {
for(int j =1;j<=n;j++) {
map[i][j] = cin.nextInt();
}
}
BBShortestPath bbp = new BBShortestPath(n,map,dist,pre);
bbp.ShortestPath(1);
for(int i =2;i<=n;i++) {
System.out.println("源點到" + i + "節點的最短距離是:"+dist[i]);
System.out.println("這個節點的前驅節點是"+ pre[i]);
}
}
private static final int INF = 1000000;
private int n ;
private int[][] map;
private int[] dist,pre; //記錄最短距離的數組,以及保存前驅頂點的數組
public BBShortestPath(int n,int[][] map,int[] dist, int[] pre) {
super();
this.n =n;
this.map= map;
this.dist = dist;
this.pre = pre;
}
//最小堆中的元素類 ID表示該活節點所表示的圖中的相應的頂點號, length表示源點到改點的距離
private static class MinHeapNode implements Comparable<MinHeapNode>{
private int id;
private int length;
public MinHeapNode(int id, int length) {
super();
this.id = id;
this.length= length;
}
@Override
public int compareTo(MinHeapNode o) { //升序排列
return length > o.length ? 1: (length == o.length ? 0 : -1);
}
}
public void ShortestPath(int s) {
LinkedList<MinHeapNode>heap= new LinkedList<MinHeapNode>();
MinHeapNode now = new MinHeapNode(s, 0);
for(int i=1;i<=n;i++) {dist[i] = INF;}
dist[s] = 0;
while(true) {
for(int j =1; j<= n; j++) {
if(map[now.id][j] !=-1 && now.length+map[now.id][j] <dist[j]) {
dist[j] = now.length+map[now.id][j];
pre[j] = now.id;
MinHeapNode next = new MinHeapNode(j, dist[j]); //加入活節點隊列中
heap.add(next);
Collections.sort(heap);
}
}
if(heap.isEmpty())break;
else now = heap.poll();
}
}
}
輸出結果