簡單談談實現遞迴暴力列舉
簡單談一談如何用遞迴實現暴力列舉。
下面先看到一個例子。
袋子裡有2紅3綠5黃球,隨機從中摸出8個,列印顯示所有組合。
暴力列舉,其實就是實現一顆搜尋樹。
那很顯然這顆搜尋樹的層數是8層,因為只要摸8個就行了。每個節點拓展出去的兒子節點很顯然是10個,因為每次都有10種選擇。
那很顯然。8層是遞迴出口。10個是每層要列舉的分支。所以可以寫出如下程式碼。
char s[]="gggrryyyyy";//存放顏色資料
char ans[10];//存放當前方案
void dfs(int d)
{
if(d==8)
{
for(int i=0;i<d;i++) printf ("%c",ans[i]);
printf("\n");
return ;
}
for(int i=0;i<10;i++)
{
ans[d]=s[i];
dfs(d+1);
}
}
但是這並不符合題目的要求。因為這樣寫,就有可能同個字元被取了多次,而很顯然每個字元只能被取一次。好的,可以修改程式碼,得到如下程式碼。
int vis[15];
char s[]="gggrryyyyy";//存放顏色資料
char ans[10];//存放當前方案
void dfs(int d)
{
if(d==8 )
{
for(int i=0;i<d;i++) printf("%c",ans[i]);
printf("\n");
return ;
}
for(int i=0;i<10;i++)
{
if(vis[i]==0)
{
vis[i]=1;
ans[d]=s[i];
dfs(d+1);
vis[i]=0;
}
}
}
這樣,加個標記資料,就可以保證在某個方案裡,一個字元只能被取一次。但是,發現,這個方法還是不行,因為一個節點發射出去的兒子節點,最多隻能是3個(“r”,”g”,”y”),而不能是(“r”,”r”,”g”,”g”“y”…),否則同種方案會被計算多次。那也就是說,在每一層搜尋的時候,搜過的字元就不能再搜了。由於字元陣列是有序的,所以用如下程式碼就可以實現上述功能。
int vis[15];
char s[]="gggrryyyyy";//存放顏色資料
char ans[10];//存放當前方案
void dfs(int d)
{
if(d==8)
{
for(int i=0;i<d;i++) printf("%c",ans[i]);
printf("\n");
return ;
}
int f=-1;
for(int i=0;i<10;i++)
{
if(vis[i]==0)
{
if(f==-1|| f!=s[i])
{
f=s[i];
vis[i]=1;
ans[d]=s[i];
dfs(d+1);
vis[i]=0;
}
}
}
}
好了,現在已經快實現了,就差最後一步了。現在有個問題就是。可能會搜出”gggrryyy”,”gggryyyr”,那由於取得是組合,所以這兩種也應該算同種方案。那如何解決這個問題呢?其實很簡單,只要保證有序地取,就不會重複,因為不同排列的同種組合排好序是一樣的,比如這次搜尋,把s第i個兒子給了ans[d],那麼下一次搜尋,列舉兒子分支的時候就從i+1開始。這樣搜出來的絕對是有序的,就不會出現無序的情況。
程式碼如下:
char s[]="gggrryyyyy";
int vis[15];
char ans[10];
void dfs(int d,int last)
{
if(d==8)
{
for(int i=0;i<8;i++) cout<<ans[i];cout<<endl;
return ;
}
int f=-1;
for(int i=last;i<10;i++)
{
if(vis[i]==0)
{
if(f==-1|| f!=s[i])
{
f=s[i];
vis[i]=1;
ans[d]=s[i];
dfs(d+1,i+1);
vis[i]=0;
}
}
}
}
主函式裡直接dfs(0,0);
就可以了。
這樣,這個問題就解決了。
下面再看到一個問題。
輸入n(1-10之間數字),將數字分解顯示,如6可以顯示為6,5+1,4+2,4+1+1…..
同樣,在寫程式碼之前,腦袋裡要有一顆搜尋樹。
但是,這裡遞迴層數就不那麼明確了。那可以用一個狀態sum來確定遞迴出口。可以寫出程式碼。
void dfs(int sum)
{
if(sum==0)
{
輸出一組解
return ;
}
for(int i=sum;i>0;i--)
{
取一個加數為i
dfs(sum-i);
}
}
很容易發現,怎麼輸出解啊。我怎麼知道加數有多少個呀?
簡單,再加個形參記錄層數就可以了,剛好第d層可以放第d個加數。
可以寫出程式碼。
void dfs(int d,int sum)
{
if(sum==0)
{
for(int i=0;i<d-1;i++) cout<<ans[i]<<"+";cout<<ans[d-1]<<endl;
return ;
}
for(int i=sum;i>0;i--)
{
ans[d]=i;
dfs(d+1,sum-i);
}
}
緊接著,你會發現你輸出了”4+2”,又輸出了”2+4”
這其實跟上面的不同排列相同組合是同一種情況,同樣可以用上面的思路解決,只要讓後面的數比前一個取的數小於等於就可以了。可以寫出程式碼。
void dfs(int d,int sum,int pre)
{
if(sum==0)
{
for(int i=0;i<d-1;i++) cout<<ans[i]<<"+";cout<<ans[d-1]<<endl;
return ;
}
for(int i=sum;i>0;i--)
{
ans[d]=i;
if(i<=pre) dfs(d+1,sum-i,i);
}
}
完美解決問題。
那現在這個問題會解決了嗎?
用遞迴實現,輸出用1分、2分和5分的硬幣湊成1元,一共有多少種方法。
不確定層數,又是組合問題,所以可以先定個sum的狀態,然後下一次取一定要大於等於上一次所取的,避免方案重複。
最後附上這三道題的完整程式碼。
摸球
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#define LL long long
using namespace std;
char s[]="gggrryyyyy";
int vis[15];
char ans[10];
void dfs(int d,int last)
{
if(d==8)
{
for(int i=0;i<8;i++) cout<<ans[i];cout<<endl;
return ;
}
int f=-1;
for(int i=last;i<10;i++)
{
if(vis[i]==0)
{
if(f==-1|| f!=s[i])
{
f=s[i];
vis[i]=1;
ans[d]=s[i];
dfs(d+1,i+1);
vis[i]=0;
}
}
}
}
using namespace std;
int main()
{
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
dfs(0,0);
return 0;
}
n的所有和
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#define LL long long
using namespace std;
int ans[10];
void dfs(int d,int sum,int pre)
{
if(sum==0)
{
for(int i=0;i<d-1;i++) cout<<ans[i]<<"+";cout<<ans[d-1]<<endl;
return ;
}
for(int i=sum;i>0;i--)
{
ans[d]=i;
if(i<=pre) dfs(d+1,sum-i,i);
}
}
using namespace std;
int main()
{
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
int n;
while(cin>>n)
dfs(0,n,n);
return 0;
}
湊硬幣
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#define LL long long
using namespace std;
int ans=0;
int a[105]={5,2,1};
void dfs(int cur,int last)
{
if(cur==0)
{
ans++;
return ;
}
for(int i=last;i<3;i++)
if(cur>=a[i]) dfs(cur-a[i],i);
}
int main()
{
//freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
int n;
for(int i=100;i<=100;i++)
{
ans=0;
dfs(i,0);
printf("%d\n",ans);
}
return 0;
}