1. 程式人生 > >五大經典演算法二 回溯

五大經典演算法二 回溯

回溯演算法在解決多選擇問題時特別有效,一般思路如下:在當前場景下,存在若干種選擇去操作,有可能兩種結果:一是違反相應條件限制,只能返回(back),另一種是該選擇選到最後居然正確並結束。故在回溯時存在三要素,能總結出這樣的三要素問題便可以迅速解決:

1 找到選擇

2 限制條件,即選擇操作在此條件下才進行

3 結束

回溯在迷宮問題等應用廣泛,下面的Leetcode22題Generate Parentheses 也是很典型的回溯問題。

Generate Parenthese要求給出n對括號下的所有可能例如

n=3

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]
從題目尋找三要素:

1 選擇:

加左括號

加右括號

2 條件

左括號沒有用完(才可以加左括號)

右括號數目小於左括號數目(才可以加右括號)

3 結束

左右括號均用完

思路:

if (左右括號都已用完) {
  加入解集,返回
}
//否則開始試各種選擇
if (還有左括號可以用) {
  加一個左括號,繼續遞迴
}
if (右括號小於左括號) {
  加一個右括號,繼續遞迴
}
程式碼:
void backtrade(string sublist,vector<string> &res,int left,int right)//left 是左括號剩餘
{
    if(left==0&&right==0){res.push_back(sublist);return;
                           }//左右括號用完
    if(left>0){backtrade(sublist+"(",res,left-1,right);}//左括號可以用
    if(left<right){backtrade(sublist+")",res,left,right-1);}
}
vector<string> generateParenthesis(int n){
    vector<string> res;
    backtrade("",res,n,n);
    return res;
                                          }

例如Leetcode 51. N-Queens
當n=4時,4X4棋盤有兩種擺法(Q代表皇后,.表示空白):

[
 [".Q..",  // Solution 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // Solution 2
  "Q...",
  "...Q",
  ".Q.."]
]
這類NP問題時間複雜度肯定是指數量級,解題思路是回溯,即通過迴圈下遞迴,條件約束回溯,直到滿足結束條件。在這裡回溯三要素:

一 選擇:新增Q還是點

二 條件:滿足NQueens佈局條件

三 所有行都滿足條件時儲存棋盤並結束

定義一個數組colForrow[n],colForrow[i]=j代表第i行第j列上是皇后。這是問題的需要耗費的空間複雜度O(N)


問題的首先需要檢查加入當前皇后的合法性:

檢查行:即當前行row不存在第二個皇后;

檢查列:即當前列不存在第二個皇后;

因為我們採用逐行求解,故在當前行佈置皇后前該行全部空白自動滿足,主要考慮列,即當前行與前面的不衝突。

檢查對角線:就是行的差和列的差的絕對值不要相等就可以

總體設計思路:

採用迴圈遞迴機制即在每一層遞迴函式內,用一個迴圈(迴圈是針對列的迴圈)把一個皇后填入對應行的每一列,如果當前加入第j列棋盤合法則遞迴處理下一行,如果在處理下一行時發現遍歷所有列都不能滿足棋盤規則,則退出該層遞迴,回溯返回到上一行。注意此時不用再把新增的元素移除,因為這裡用的是一個一維陣列去存皇后在對應行的哪一列,當回溯到上一行時該陣列自動回到原來狀態。在進入上一行後,由於迴圈將列數j加1繼續判斷。直到所有行都完成儲存結果並結束

//Queens
string vectorcharTostring(vector<char> ss){
    string res="";
    for(int i=0;i<ss.size();i++)
	res+=ss[i];
    res+='\0';
    return res;
                                            }
bool checkValidQueen(int row,int colForrow[])
{
for(int i=0;i<row;i++)
    if(colForrow[row]==colForrow[i]||abs(colForrow[row]-colForrow[i])==row-i)return 0;
return 1;
}//除了剛加的,前面的都是合法,故只需檢查當前行和前面
void helper_queen(int n,int row,int colForrow[], vector< vector<string> >& res)
{
    if(row==n){
	vector<string> item;
	for(int i=0;i<n;i++)
	{
	    vector<char> strRow;
	    for(int j=0;j<n;j++)
		if(colForrow[i]==j)strRow.push_back('Q');
	        else strRow.push_back('.');
	   string tmp=vectorcharTostring(strRow);
	   item.push_back(tmp);
	}
	//for(int i=0;i<item.size();i++)cout<<item[i]<<endl;
	res.push_back(item);//儲存完畢
	return;
               }
    for(int i=0;i<n;i++)
    {
	colForrow[row]=i;
	if(checkValidQueen(row,colForrow))
	    helper_queen(n,row+1,colForrow,res);
    }
}//逐行求解,在每一行嘗試每個列位置
vector< vector<string> > solveNQueens(int n){
vector< vector<string> >res;
int * colForrow=(int*)malloc(sizeof(int)*n);
helper_queen(n,0,colForrow,res);
return res;
                                             }
以n=4來簡單描述思路:



 再例如39. Combination Sum
給定一候選序列和一個目標值,要求從候選序列中選出若干資料使得它們之和與目標值相等,這裡每個選出的資料可以被重複使用。還是回溯思想:

一 選擇:選擇一個元素

二 條件:

三 直到和與taget相等

設計思路:

也是迴圈遞迴方式,每次遞迴中某次迴圈i時選擇candidate[i]元素,把剩下的元素一一加到結果集合中,並且把目標減去加入的元素,然後把剩下元素(由於可以重複使用故包括當前加入的元素)放到下一層遞迴中解決子問題。若得到的差值小於0,則失敗,需要增加該遞迴中的迴圈i,迴圈結束返回上一層函式,此時需要清空已經放入結果集的元素,該層i自增。若最終得到差值為0則儲存結果。

void sift(vector<int>&nums,int low,int high){
    int i=low,j=2*i+1;
    if(low<high){
	int tmp=nums[low];
	while(j<=high){
	    if(j+1<=high&&nums[j]<nums[j+1])j++;
	    if(nums[j]>tmp){nums[i]=nums[j];i=j;j=2*i+1;}
	    else break;
	              }
	nums[i]=tmp;
                  }
                                            }
void sort_heap(vector<int>&num){
    int n=num.size();
    for(int i=n/2-1;i>=0;i--)
	sift(num,i,n-1);
    for(int i=n-1;i>=1;i--){int t=num[0];num[0]=num[i];num[i]=t;sift(num,0,i-1);}
    //for(int i=0;i<num.size();i++)cout<<num[i]<<" ";
                                }
//combinationsum
void helper_combination(vector<int> candidates,int start,int target,vector<int> item,vector< vector<int> >&res){
if(target<0)return;
if(target==0){res.push_back(item);return;}//達到結束
for(int i=start;i<candidates.size();i++){
   if(i-1>=0&&candidates[i]==candidates[i-1])continue;//因為可以重複使用,故集合裡的重複元素沒有什麼用
   item.push_back(candidates[i]);
   helper_combination(candidates,i,target-candidates[i],item,res);
   item.pop_back();//這裡需要刪除
                                     }
                                                                                    }
vector< vector<int> > combinationSum(vector<int>& candidates,int target){
sort_heap(candidates);
vector< vector<int> >res;
vector<int> item;
helper_combination(candidates,0,target,item,res);
return res;
}
舉例如下:

候選集合[2,3,6,7],target=7

正確結果:

[
  [7],
  [2, 2, 3]
]

大概思路:

77. Combinations
也類似思路,給出n,從1-n選出k個組成一對,求出所有這樣的對

例如

n=4,k=2:

[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
也是迴圈遞迴,不合適回溯上一遞迴。
/combinations
void helper_combine(int n,int k,int start,vector<int> item,vector< vector<int> >&res){
if(item.size()==k){res.push_back(item);return;}
for(int i=start;i<=n;i++)
{item.push_back(i);
    helper_combine(n,k,i+1,item,res);
 item.pop_back();
}
}
vector< vector<int> >combine(int n,int k){
vector< vector<int> >res;
if(n<1||k>n)return res;
vector<int> item;
helper_combine(n,k,1,item,res);
return res;
}
對於46. Permutations 也一人如此

對於只給出數字n:

//permutation
void helper_Npermutation(int n,int start,vector<int> item){
if(n==item.size()){for(int i=0;i<n;i++)cout<<item[i]<<" ";cout<<endl;}
else{
    for(int i=1;i<=n;i++)
    {   int f=1;
	for(int j=0;j<start;j++) if(item[j]==i)f=0;
        if(f){
	item.push_back(i);
	helper_Npermutation(n,i+1,item);
	item.pop_back();}
    }
     }
}
void getNpermutation(int n){
if(n<1)return;
vector<int>item;
helper_Npermutation(n,0,item);
}
對於給出一個非重複序列:
void helper_permutation(vector<int> candidates,int start,vector<int> item,vector< vector<int> >&res){
if(item.size()==candidates.size()){res.push_back(item);return;}
int n=candidates.size();
for(int i=0;i<n;i++)
{int f=1;
    for(int j=0;j<item.size();j++)if(item[j]==candidates[i])f=0;
 if(f){
     item.push_back(candidates[i]);
     helper_permutation(candidates,i+1,item,res);
     item.pop_back();
       }
}
}
vector< vector<int> > permute(vector<int>& nums){
vector< vector<int> >res;
if(nums.size()<1)return res;
vector<int>item;
helper_permutation(nums,0,item,res);
return res;
}
對於給出一個重複序列:
主要先要排序:
void helper_permutation(vector<int> candidates,int start,vector<int> item,vector< vector<int> >&res){
if(item.size()==candidates.size()){res.push_back(item);return;}
int n=candidates.size();
for(int i=0;i<n;i++)
{   if(!i||candidates[i]!=candidates[i-1]){
    int f1=0,f2=0;
    for(int j=0;j<item.size();j++)if(item[j]==candidates[i])f1++;
    for(int j=0;j<n;j++)if(candidates[j]==candidates[i])f2++;
 if(f2>f1){
     item.push_back(candidates[i]);
     helper_permutation(candidates,i+1,item,res);
     item.pop_back();
       }
}
}
}
vector< vector<int> > permuteUnique(vector<int>& nums){
vector< vector<int> >res;
if(nums.size()<1)return res;
vector<int>item;
sort(nums.begin(),nums.end());//include <algorithm>
helper_permutation(nums,0,item,res);
return res;
}


140. Word Break II

string vectorcharTostring(vector<char> ss){
    string res="";
    for(int i=0;i<ss.size();i++)
	res+=ss[i];
    //res+='\0';
    return res;
                                            }
bool find_vector(vector<string> ss,string s){
int i=0;
while(i<ss.size()){if(s.compare(ss[i])==0)return 1;i++;}
return 0;
}
//word break2
void helper_wordbreak(string s,vector<string> wordDict,int start,string item,vector<string>& res){
if(start>=s.length()){res.push_back(item);item.clear();return;}
    vector<char> t;
for(int i=start;i<s.length();i++)
{

    t.push_back(s[i]);
   // cout<<vectorcharTostring(t).size()<<endl;
     if(find_vector(wordDict,vectorcharTostring(t)))
   {
    //item+=(vectorcharTostring(t));
    //if(i<s.length()-1)item+=" ";
    string newitem=item.length()?(item+" "+vectorcharTostring(t)):vectorcharTostring(t);
    helper_wordbreak(s,wordDict,i+1,newitem,res);
    
   }
   
}
}
vector<string> wordBreak(string s, vector<string>& wordDict){
vector<string> res;
if(wordDict.size()<1)return res;
string item="";
helper_wordbreak(s,wordDict,0,item,res);
return res;
}
這種類似以前NQueen,Permutation做法正確,但是會超時Time Limit Exceeded!故需要完善。結合139. Word Break 題採用的動態規劃做法,這裡也可以引入,故程式碼修改如下:
bool find_vector(vector<string> ss,string s){
int i=0;
while(i<ss.size()){if(s.compare(ss[i])==0)return 1;i++;}
return 0;
}
//word break2
void helper_wordbreak(string s,vector<string> wordDict,int start,string item,vector<string>& res,vector<bool> dp){
 string t;
for(int i=1;i+start<=s.length();i++)
{
  if(dp[i+start]&&find_vector(wordDict,s.substr(start,i))){
      t=s.substr(start,i);
      if(i+start<s.length())helper_wordbreak(s,wordDict,i+start,item+t+" ",res,dp);
      else res.push_back(item+t);
                                                           }
}
}
vector<string> wordBreak(string s, vector<string>& wordDict){
vector<string> res;
if(wordDict.size()<1)return res;
string item="";
vector<bool> dp(s.length()+1,false);
dp[0]=true;
int min_len=INT_MAX,max_len=0;
for(int i=0;i<wordDict.size();i++){if(min_len>wordDict[i].size())min_len=wordDict[i].size();
				   if(max_len<wordDict[i].size())max_len=wordDict[i].size();
				  }

for(int i=0;i<s.size();i++){
if(dp[i]){for(int len=min_len;i+len<=s.size()&&len<=max_len;len++)
	     if(find_vector(wordDict,s.substr(i,len)))dp[i+len]=1;
	  }
			    }
if(dp[s.length()])
helper_wordbreak(s,wordDict,0,item,res,dp);
return res;
}
131. Palindrome Partitioning

給定一個字串,把內部分部,使得每部分子串也是迴文串。

首先需要得到字串所有字串是否是迴文,這裡採用動態遞迴法,引入二維陣列bool dp[i][j],表示從i到j這一子串是否是迴文,下面是遞迴公式

dp[i][j]=1 when s[i]==s[j]&&j-i==1(即相鄰)或者dp[i+1][j-1]=1

dp[i][j]=0;

故初始值為0。

vector <vector <bool> > getdict(string s){
vector< vector<bool> > res;
for(int i=0;i<s.length();i++){
    vector<bool> aa;
    for(int j=0;j<s.length();j++){aa.push_back(0);};
    res.push_back(aa);
                              }
if(s.size()<1)return res;
for(int i=s.length()-1;i>=0;i--)
    for(int j=i;j<s.length();j++)
	if(s[i]==s[j]&&((j-i<2)||res[i+1][j-1]))res[i][j]=1;

return res;
                                          } //判斷任意字串之間是不是迴文
剩下的很明顯也是迴圈遞迴,不合適回溯即可:
//palindrome Partitioning
vector <vector <bool> > getdict(string s){
vector< vector<bool> > res;
for(int i=0;i<s.length();i++){
    vector<bool> aa;
    for(int j=0;j<s.length();j++){aa.push_back(0);};
    res.push_back(aa);
                              }
if(s.size()<1)return res;
for(int i=s.length()-1;i>=0;i--)
    for(int j=i;j<s.length();j++)
	if(s[i]==s[j]&&((j-i<2)||res[i+1][j-1]))res[i][j]=1;

return res;
                                          } //判斷任意字串之間是不是迴文
void helper_partitioning(string s,vector< vector<bool> > dict,int start, vector<string> item,vector< vector<string> > &res){
if(start==s.length()){res.push_back(item);return;}
for(int i=start;i<s.length();i++)
{if(dict[start][i]){item.push_back(s.substr(start,(i-start+1)));helper_partitioning(s,dict,i+1,item,res);item.pop_back();}
}
}
vector< vector<string> > partition(string s){
vector< vector<string> >res;
if(s.length()<1)return res;
vector< vector<bool> >dict;
dict=getdict(s);
vector<string> item;
helper_partitioning(s,dict,0,item,res);
return res;
}

93. Restore IP Addresses

給定一個只含數字的字串,判斷所能得到的 所有IP地址

例如:

s="25525511135"

則存在["255.255.11.135", "255.255.111.35"]

首先判斷IP地址要求的格式:

1 一定含有點分開的四段子欄位,每段字元數最多3個

2 每個子欄位轉換成int範圍是0-255

3 每個字串可以是0,但不能是00,01,0XX

同樣運用回溯,次取1,2,3長度的子串,判斷其為合法的IP地址子欄位後,取後面的真個字串遞迴判斷,通過count從0增加到3來判斷是否完成4個子段的判斷,將結果加入最終的res中去。

bool isValidsubstr(string substr1){
if(substr1[0]=='0'){return substr1=="0";
                   }
int res=atoi(substr1.c_str());
return res>=0&&res<=255;
}//判斷字串合法
void helper_ipaddress(string s,int start,string item,vector<string>&res){
if(start==3&&isValidsubstr(s)){res.push_back(item+s);return;}
for(int i=1;i<=3&&i<s.size();i++){
    string sub=s.substr(0,i);
    if(isValidsubstr(sub)){helper_ipaddress(s.substr(i),start+1,item+sub+".",res);}
                             }
}
vector<string> restoreIpAddresses(string s){
vector<string> res;
if(s.size()<1||s.size()<4||s.size()>12)return res;
string item="";
helper_ipaddress(s,0,item,res);
return res;
}