算法——回溯法(子集、全排列、皇后問題)


參考:http://www.cnblogs.com/wuyuegb2312/p/3273337.html#intro
參考:《算法競賽入門經典》 P120

1、定義

回溯算法也叫試探法,它是一種系統地搜索問題的解的方法。
回溯算法的基本思想是:從一條路往前走,能進則進,不能進則退回來,換一條路再試。
回溯算法解決問題的一般步驟為:
1、定義一個解空間,它包含問題的解。
2、利用適於搜索的方法組織解空間。
3、利用深度優先法搜索解空間。
4、利用限界函數避免移動到不可能產生解的子空間。
問題的解空間通常是在搜索問題的解的過程中動態產生的,這是回溯算法的一個重要特性。

確定了解空間的組織結構后,回溯法就從開始結點(根結點)出發,以深度優先的方式搜索整個解空間。這個開始結點就成為一個活結點,同時也成為當前的擴展結點。在當前的擴展結點處,搜索向縱深方向移至一個新結點。這個新結點就成為一個新的活結點,並成為當前擴展結點。如果在當前的擴展結點處不能再向縱深方向移動,則當前擴展結點就成為死結點。此時,應往回移動(回溯)至最近的一個活結點處,並使這個活結點成為當前的擴展結點。
回溯法即以這種工作方式遞歸地在解空間中搜索,直至找到所要求的解或解空間中已沒有活結點時為止。

/*
對於其中的函數和變量,解釋如下:
a[]表示當前獲得的部分解;
k表示搜索深度;
input表示用於傳遞的更多的參數;
is_a_solution(a,k,input)判斷當前的部分解向量a[1...k]是否是一個符合條件的解
construct_candidates(a,k,input,c,ncandidates)根據目前狀態,構造這一步可能的選擇,存入c[]數組,其長度存入ncandidates
process_solution(a,k,input)對於符合條件的解進行處理,通常是輸出、計數等
make_move(a,k,input)和unmake_move(a,k,input)前者將采取的選擇更新到原始數據結構上,后者把這一行為撤銷。
*/
bool finished = FALSE; /* 是否獲得全部解? */
backtrack(int a[], int k, data input)
{
int c[MAXCANDIDATES]; /*這次搜索的候選 */
int ncandidates; /* 候選數目 */
int i; /* counter */
if (is_a_solution(a,k,input))
process_solution(a,k,input);
else
{
k = k+1;
construct_candidates(a,k,input,c,&ncandidates);
for (i=0; i<ncandidates; i++)
{
a[k] = c[i];
make_move(a,k,input);
backtrack(a,k,input);
unmake_move(a,k,input);
if (finished)
return; /* 如果符合終止條件就提前退出 */
}
}
}

2、給出數組,求全排列

《算法競賽入門經典》 P116
給出一個數組,將其按字典序形成全排列。
例如給出:a[] = {3, 1, 2};
我們可以先將其排序,a[] = {1,2,3};然后再全排列。

1 2 3 
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2

這里有對全排列的一個綜合的概括:http://blog.csdn.net/morewindows/article/details/7370155/
1.全排列就是從第一個數字起每個數分別與它后面的數字交換。
2.去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。
3.全排列的非遞歸就是由后向前找替換數和替換點,然后由后向前找第一個比替換數大的數與替換數交換,最后顛倒替換點后的所有數據。

1:遞歸方法
分別將每個位置交換到最前面位,之后全排列剩下的位。

【例】遞歸全排列 1 2 3 4 5
1,for循環將每個位置的數據交換到第一位
swap(1,1~5)
2,按相同的方式全排列剩余的位

/// 遞歸方式生成全排列的方法
//fromIndex:全排列的起始位置
//endIndex:全排列的終止位置
void PermutationList(int fromIndex, int endIndex)
{
if (fromIndex > endIndex)
Output(); //打印當前排列
else
{
for (int index = fromIndex; index <= endIndex; ++index)
{
// 此處排序主要是為了生成字典序全排列,否則遞歸會打亂字典序
Sort(fromIndex, endIndex);
Swap(fromIndex, index);
PermutationList(fromIndex + 1, endIndex);
Swap(fromIndex, index);
}
}
}

添加排序函數

int comp2(const void*a,const void*b)
{
return *(int*)a - *(int*)b;
}

int list[] = {4, 3, 1, 2, 5};
qsort(list,5,sizeof(int),comp2);

2、非遞歸方法(字典序法):
這種算法被用在了C++的STL庫中。
  對給定的字符集中的字符規定了一個先后關系,在此基礎上規定兩個全排列的先后是從左到右逐個比較對應的字符的先后。

 [例]字符集{1,2,3},較小的數字較先,這樣按字典序生成的全排列是:
    123,132,213,231,312,321
※ 一個全排列可看做一個字符串,字符串可有前綴、后綴。
  生成給定全排列的下一個排列.所謂一個的下一個就是這一個與下一個之間沒有其他的。這就要求這一個與下一個有盡可能長的共同前綴,也即變化限制在盡可能短的后綴上。

[例]839647521是1–9的排列。1—9的排列最前面的是123456789,最后面的987654321,從右向左掃描若都是增的,就到了987654321,也就沒有下一個了。否則找出第一次出現下降的位置。

【例】 一般而言,設P是[1,n]的一個全排列。
      P=P1P2…Pn=P1P2…Pj-1PjPj+1…Pk-1PkPk+1…Pn
    find:  j=max{i|Pi

void PermutationList(int *list, int n)
{
int i,j,k,diff;
int all_increase = 0;

qsort(list,n,sizeof(int),comp2);

for(i=0;i<n;i++)
printf("%d ",list[i]);
printf("\n");

while(1)
{
//從尾部往前找第一個P(i-1) < P(i)的位置
for(i=n-1; i>0; i--)
{
if(list[i-1] < list[i])
break;
}
if(i == 0) //從尾部開始全部為增序時,全排列結束
break;

//從i位置往后找到最后一個大於i-1位置的數
//即其差最小
diff = list[i] - list[i-1];
k = i;
for(j=i; j<n; j++)
{
if(list[j] > list[i-1] && diff > list[j]-list[i-1])
{
diff = list[j] - list[i-1];
k = j;
}
}
//交換位置i-1和k的值
swap2(list+i-1,list+k);
//倒序i后的數
for(j=i,k=n-1; j< k; j++,k--)
{
swap2(list+j,list+k);
}
for(i=0;i<n;i++)
printf("%d ",list[i]);
printf("\n");
}
}

參考:http://www.cnblogs.com/pmars/archive/2013/12/04/3458289.html

3、給出n,求1~n的全排列

《算法競賽入門經典》 P116
輸入一個整數,例如3,生成1、2、3的全排列。
輸出為:

    1 2 3 
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

示意圖:
這里寫圖片描述
代碼:

void print_permutation(int *a, int n, int cur)
{
int i,j;

if(cur == n)
{
for(i=0;i<n;i++)
printf("%d ",a[i]);
printf("\n");
}
else
{
int exist = 0;
for(i=1;i<=n;i++) //嘗試在a[cur]中填入1~n的數
{
exist = 0;
for(j=0;j<cur;j++)
{
if(i == a[j])
exist = 1;
}

if(!exist)
{
a[cur] = i;
print_permutation(a,n,cur+1);
}
}
}
}

4、子集生成

《算法競賽入門經典》 P120
給定一個n,枚舉0~n的所有子集。
例如:n=4,枚舉0,1,2,3,4的子集。
子集:

    0 
0 1
0 1 2
0 1 2 3
0 1 3
0 2
0 2 3
0 3
1
1 2
1 2 3
1 3
2
2 3
3

1、增量構造法

/*遞歸輸出0~n所有子集,其中cur為當前下標,cur-1表示上一層最后一個元素下標,初始值0*/ 
void print_subset(int *a, int n, int cur)
{
int i,min;
//for(i=0;i<cur;i++)
for(i=0;i<=cur-1;i++) //輸出上一層子集0~cur-1的位置。
printf("%d ",a[i]);
printf("\n");

//找到當前子集首個值,因為按字典順序輸出,所以每次找到最小的元素
min = cur ? a[cur-1]+1 : 0; //每次獲得當前集的最小元素,要么是0,要么是上一層最后一個元素+1

/*************************************************************/
/* 以上為每次遞歸都會執行的兩個操作,1、輸出上一子集 2、找到當前子集的首個值 */

for(i=min;i<n;i++) //循環處理:當前集的最小元素min~n-1的元素
{
a[cur] = i; //將當前子集的最小值,也就是第一個元素給a[cur]
print_subset(a,n,cur+1); //遞歸,擴大當前子集的范圍cur+1,在遞歸中輸出遞歸前的前一個子集的集合
}
}

這里寫圖片描述

2、二進制法

/*輸出子集s*/
/*
* 當s=151=1001 0111 時,輸出為0 1 2 4 7(子集)
* n表示為總集合
* 當n=8
* s= 0000 0001 時,輸出為0
*/

void print_subsetn(int n, int s)
{
int i;
for(i=0; i<n; i++)
{
if(s & (1<<i)) //如果s中的i位為1
printf("%d ",i); //打印s中位數為1的位數號i
}
printf("\n");
}

/*輸出0~n所有子集*/
void print_subset(int n)
{
int i;
for(i=0; i<(1<<n); i++) //i表示子集,0~2^n-1從空集到全集的范圍,全集{0,1,...,n-1}二進制為n個1,即2^n-1
print_subsetn(n,i);
}
/*
* 當n=2時,二進制法的二進制長度就為2
* 那么空集=0 全集=3
* i=0=00 => 輸出空集
* i=1=01 => 輸出{0}
* i=2=10 => 輸出{1}
* i=3=11 => 輸出{0,1}
*/

5、從n個數中選m個數的組合

1、n個數存放在數組b中。
【思路】
從后往前選,先確定一個數,例如在b[4] = {1,2,3,4}中,先確定4,然后再在剩下的1,2,3中選m-1個數,這是一個遞歸的形式。
例如:在1 2 3 4 5中選3個數
先確定5,然后在1 2 3 4 中選2個數
5完成后,確定4,再在1 2 3 中選2個數,如此遞歸。

//在b數組中選m個數
//n: b數組的個數 j為全局數組a的指針。
void choose(int *b,int n, int m, int j)
{
int i,k;
if(m == 0)
{
for(i=0;i<j;i++)
printf("%d",a[i]);
printf("\n");
return;
}
for(i=n; i>0;i--) //從后往前選
{
a[j] = b[i-1];
//choose(b,n-1,m-1,j+1); //在1~n-1中選取m-1個數
choose(b,i-1,m-1,j+1); //將n-1改為i-1解決了出現重復數字的情況,例如:43 42 41 33 32 31 22 21 11
}
}

2、從1~n中選m個數的組合,不利用數組b

//在1~n中選m個數
void choose(int n, int m, int j)
{
int i,k;
if(m == 0)
{
for(i=0;i<j;i++)
printf("%d",a[i]);
printf("\n");
return;
}
for(i=n; i>0;i--) //從后往前選
{
a[j] = i;
choose(i-1,m-1,j+1);
}
}

參考: http://blog.csdn.net/wumuzi520/article/details/8087501

6、在1-n中選取m個字符進行全排列

int vis[11];

void chos(int n, int m, int j)
{
int i;

//if(m == 0)
if(m == j)
{
for(i=0;i<j;i++)
printf("%d ",a[i]);
printf("\n");
}
for(i=1; i<=n; i++)
{
if(!vis[i])
{
a[j] = i;
vis[i] = 1;

//chos(n-1,m-1,j+1);
chos(n,m,j+1); //因為有vis數組判斷某個數是否已經加入到輸出的集合了,所有不需要n-1

vis[i] = 0;
}
}

return;
}

ACM:http://acm.nyist.net/JudgeOnline/problem.php?pid=19

八皇后問題

《算法競賽入門經典》 P125
在8* 8的棋盤上擺放8個皇后,使其不能互相攻擊,即任意的兩個皇后不能處在同意行,同一列,或同一斜線上(不能在一條左斜線上,當然也不能在一條右斜線上)。可以把八皇后問題拓展為n皇后問題,即在n*n的棋盤上擺放n個皇后,使其任意兩個皇后都不能處於同一行、同一列或同一斜線上。

    //一個N皇后問題的處理
void Queen(int j, int (*Q)[N])
{
if(j == N) //j從0開始一步一步往右邊逼近,當到達N時,前面的都已放好。
{
//得到一個解,輸出數組Q
return;
}

//對j列的每一行進行探測,看是否能夠放置皇后
for(int i=0; i<N; ++i) //i表示行,j表示列
{
if(isCorrect(i,j,Q)) //如果可以在i行,j列中放置皇后(判斷同行同列斜線上是否已有皇后)
{
Q[i][j] = 1; //放置皇后
Queen(j+1,Q); //深度遞歸,繼續放下一列
Q[i][j] = 0; //回溯
}
}
}
/*
* n皇后處理
* cur表示行,col[cur]表示第cur行皇后的列編號,tot表示解的個數
* 算法的過程:起始列按0~n-1,每次按行放置(cur初始值為0),循環尋找從第0列~第n-1列能放置皇后的位置,找到后進行下一
* 行的放置。
* 當所有行都放置成功后,解的個數加一。
*/

int tot = 0;
int col[50];

void search(int n, int cur)
{
int i,j,ok;

if(cur == n) //行數達到最大
{
tot++;
}
else
{
for(i=0; i<n; i++) //列
{
ok = 1; //放置標志位
col[cur] = i; //嘗試將第cur行的皇后放在第i列

for(j=0; j<cur; j++) //從第0行開始到cur-1行,檢查是否和前面的皇后有沖突
{
if(col[cur] == col[j] || //在同一列
cur-col[cur] == j-col[j] || //主對角線
cur+col[cur] == j+col[j] ) //副對角線
{
ok = 0;
break; //當前i列存在沖突,換下一列
}
}

if(ok) //如果當前行cur找到放置點后,繼續下一行的放置
{
search(n,cur+1);
}
}
}
}
int main()
{
int n;
scanf("%d",&n);

search(n,0);
printf("%d\n",tot);
}
八皇后問題就是回溯算法的典型,第一步按照順序放一個皇后,然后第二步符合要求放第2個皇后,如果沒有位置符合要求,那么就要改變第一個皇后的位置,重新放第2個皇后的位置,直到找到符合條件的位置就可以了。
int vis[3][];  //記錄當前嘗試的皇后所在的列|正斜線|負斜線是否已有其他皇后
void search_ex(int n, int cur)
{
int i,j,ok;

if(cur == n) //行數達到最大
{
tot++;
}
else
{
for(i=0; i<n; i++) //列
{
if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n]) //可放置
{
col[cur] = i; //嘗試將第cur行的皇后放在第i列
vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 1;
search_ex(n,cur+1);
vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 0;
}
}
}
}

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com