模板
void dfs(int x)
{
if(到達目的地)
{
if(解合法)
輸出解;
return;
}
for(枚舉選擇個數)
{
if(合法)
{
保存結果;
更改狀態;
dfs(下一步);
回溯;
}
}
}
例題
例1
生成k的n維向量(1<=k<=10,1<=n<=6)
n維向量是有n個元素的序對,每數的取值範圍從1到k
輸入k和n,輸出所有k的n維向量
譬如2的3維向量
有{1,1,1}{1,1,2}{1,2,1}{1,2,2}{2,1,1}{2,1,2}{2,2,1}{2,2,2}
譬如3的5維向量
有{1,1,1,1,1}{1,1,1,1,2}{1,1,1,1,3}{1,1,1,2,1}······{3,3,3,3,2}{3,3,3,3,3}
分析
可以把每一次構造的分成n個階段
一階段:從k個數中選一個數出來
二階段:從k個數中選一個數出來
三階段:從k個數中選一個數出來
·············
n階段:從k個數中選一個數出來
n個階段後,就輸出解
代碼
#include<cstdio>
#define M 5
int ans[M+5],n,k;
void dfs(int x)
{
if(x>n)//判斷是否可以輸出解
{
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
for(int i=1;i<=k;i++)
{
ans[x]=i;//保存結果
dfs(x+1);//進入下一階段
}
}
int main()
{
scanf("%d%d",&k,&n);
dfs(1);
return 0;
}
例2
如果在例1的基礎上,在每個方案前加上序號,又該怎麼寫?
分析
只用加上計數器就可以了,輸出序號後,讓序號加1;
代碼實現如下
#include<cstdio>
#define M 5
int ans[M+5],n,k,cnt;
void dfs(int x)
{
if(x>n)
{
cnt++;//自加
printf("%d:",cnt);//輸出序號
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
for(int i=1;i<=k;i++)
{
ans[x]=i;
dfs(x+1);
}
}
int main()
{
scanf("%d%d",&k,&n);
dfs(1);
return 0;
}
例3
如果將向量改成1到n的全排列呢?
輸入n,輸出1到n的全排列
分析
只需加一個check,搜鎖即將放的數在之前有沒有放過。
代碼
#include<cstdio>
#define M 5
int ans[M+5],n,cnt;
bool check(int v,int u)//查找函數
{
for(int i=1;i<=u;i++)
if(ans[i]==v)return 0;//如果一樣,返回0
return 1;
}
void dfs(int x)
{
if(x>n)
{
printf("%d:",++cnt);
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
for(int i=1;i<=n;i++)
if(check(i,x))
{
ans[x]=i;
dfs(x+1);
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
大家可以試試輸入7會發生什麼,8呢?9呢?
顯然程序“變慢”了,那麼該如何解決呢
其實,將用過的數標記就可以了,但回溯時一定改回來
代碼如下
#include<cstdio>
#define M 5
int ans[M+5],n,cnt;
bool vis[M+5];
void dfs(int x)
{
if(x>n)
{
printf("%d:",++cnt);
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
for(int i=1;i<=n;i++)
if(!vis[i])//如果沒使用過
{
vis[i]=true;//標記使用過
ans[x]=i;
dfs(x+1);
vis[i]=false;//記得回溯
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
時間肯定快了,但仍然看不出效果,接下來介紹一種最快的算法。
大家發現,每次只用交換兩數的位置就可以生成新排列。
交換時注意交換過的不要再交換。所以從當前開始往後交換。
代碼實現如下
#include<cstdio>
#include<algorithm>
using namespace std;
#define M 10
int n,cnt,ans[M+5]={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14};//先把數按從1到n存進去
void dfs(int x)
{
if(x>n)
{
printf("%d:",++cnt);
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
for(int i=x;i<=n;i++)//與後面的數交換,前面的數交換過
{
swap(ans[x],ans[i]);//交換這兩個數
dfs(x+1);
swap(ans[x],ans[i]);//一定記得交換回來
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
這裏提一下,這種算法沒有按字典序排,如果題目要求字典序輸出,就用第二種。
如例4就需要字典序
例4
輸入一個包含 n個非負數的數組,元素可以重複。按字典序輸出所有全排列方案,要求不重複。
(1<=n<=10)
看起來複雜,一會重複一會不重複的,但跟前面差不多,只需多定義一個變量las,用來儲存當層函數上一次填的數,如果一樣,顯然重複,就繼續往後循環,但別忘了更改las的值
代碼如下
#include<cstdio>
#define M 10
int ans[M+5],n,cnt,a[M+5];
bool vis[M+5];
void dfs(int x)
{
if(x>n)
{
printf("%d:",++cnt);
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
int las=-1;//輸入非負數,所以las初值爲負數
for(int i=1;i<=n;i++)//枚舉n個數
if(!vis[i]&&las!=a[i])
{
vis[i]=true;
ans[x]=a[i];
las=a[i];//更改las的值
dfs(x+1);
vis[i]=false;
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
dfs(1);
return 0;
}
另一種方法針對每個數範圍小
將數字的種類表示出來,這種方法對空間需求較高
代碼如下
#include<cstdio>
#define M 10
#define N 50
int ans[M+5],n,cnt,a[M+5];
int c[N+5];//假設每個數不超過50,存每種數可以使用的次數
void dfs(int x)
{
if(x>n)
{
printf("%d:",++cnt);
for(int i=1;i<n;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[n]);
return;
}
for(int i=0;i<=N;i++)//枚舉數的種類
if(c[i])
{
c[i]--;//使用數
ans[x]=i;
dfs(x+1);
c[i]++;//回收數
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
c[a[i]]++;//初始化
}
dfs(1);
return 0;
}
例5
STL標準庫中的next_permutation()函數可以生成下一個排列,結合循環就可以將從當前排列開始的所有排列生成出來。所以,使用前往往會先排序,且一般把它放到while()循環當中。(需要頭文件algorithm)
代碼如下
#include<cstdio>
#include<algorithm>
using namespace std;//調用STL函數
#define M 10
int a[M],cnt;
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+1+n);//生成最小排列(從小到大排序)
do
{
printf("%d:",++cnt);
for(int i=1;i<n;i++)
printf("%d ",a[i]);
printf("%d\n",a[n]);
}while(next_permutation(a+1,a+1+n));
return 0;
}