回溯算法:子集樹和排列樹

假設現在有一列數a[0],a[1], ...a[n-1]

①如果一個問題的解的長度不是固定的,並且解和元素順序無關,即可以從中選擇0個或多個,那麼解空間的個數將是指數級別的,爲2^n,可以用下面的子集樹來表示所有的解(假設這裏n=4)



PIC. 子集樹

子集樹的算法框架爲


void backtrack(int t) {//表示訪問到第t層,t從0開始
if (t == n) //如上圖(PIC. 子集樹)n = 4的時候就可以輸出解了
 output(x);
else
 for (int i = 0; i <= l; i++) { //表示選或不選a[t]
  x[t] = i;
  if (constraint(t) && bound(t))
   backtrack(t + 1);
 }
}

②如果解空間是由n個元素的排列形成,也就是說n個元素的每一個排列都是解空間中的一個元素,那麼,最後解空間的組織形式是排列樹




PIC.排列樹

排列樹算法的基本框架爲

模板一

void backtrack(int t)
{
    if (t == n)
        output(x);
    else
        for (int i = t; i < n; i++)
        {
            swap(x[t], x[i]);
            if (constraint(t) && bound(t))
            {
                backtrack(t + 1);
                swap(x[t], x[i]);
            }
        }
}

排列數舉例:求1,2,3,4,5所有的排列

#include <iostream>
using namespace std;
int x[5] = {1, 2, 3, 4, 5};
int n = 5;

void swap(int& a, int& b) {
  int tmp = a;
  a = b;
  b = tmp;
}
void BackTrack(int t) {
  if (t == n) {
    for (int i = 0; i < n; i++)
      cout << x[i] << " ";
    cout << endl;
  }
  else
    for (int i = t; i < n; i++) {
      swap(x[t], x[i]);
      BackTrack(t + 1);
      swap(x[t], x[i]);
    }
}
int main() {
  BackTrack(0);
  return 0;
}

變種:求解滿足a-b+c-d+e = m的一個排列,找到一個就返回

#include <iostream>
using namespace std;
int x[5] = {1, 2, 3, 4, 5};
int n = 5;
int m;


void swap(int& a, int& b) {
  int tmp = a;
  a = b;
  b = tmp;
}
bool BackTrack(int t) {
  if (t == n) {
    int sum = 0;
    int flag = 1;
    for (int i = 0; i < n; i++) {
      sum += x[i] * flag;
      flag = -flag;
    }
    if (sum == m) return true;
  }
  else
    for (int i = t; i < n; i++) {
      swap(x[t], x[i]);
      if (BackTrack(t + 1))
        return true;
      swap(x[t], x[i]);
    }
  return false;
}
int main() {
  m = 1;
  if(BackTrack(0)) {
    cout << "找到排列滿足條件: ";
    for (int i = 0; i < n; i++) {
      cout << x[i] << " ";
    }
    cout << endl;
  } else {
    cout << "不存在這樣的排列" << endl;
  }
  return 0;
}


遇到子集樹和排列數的回溯問題,幾乎都可以用上面的模板來套


續:排列樹的模板相對於子集樹來說比較難理解,我也是一直沒有深入去理解它的結構,但是,自從看了LRJ的《算法競賽入門經典》之後關於排列的創建,突然覺得眼前一亮,真的是非常容易理解,僞代碼如下

注意: 序列A表示“前綴”序列,以便輸出,S表示需要進行全排列的元素集合,以便依次選做第一個元素

void print_emu(序列 A, 集合S) {
  if (序列A滿了) 輸出序列A;
 else {
  1.按照從小到大依次考慮S中的每一個元素v
   2.print_emu(在A的末尾加上v後得到新的序列, S-{v})
 }
}

以上代碼可以按字典輸出n個數的全排列

c++代碼如下

模板二

#include <iostream>
using namespace std;
//cur表示當前序列A中的元素爲A[0], A[1],...A[cur-1]
int print_emu(int A[], int n, int cur) {
  if (cur == n) {
    for (int i = 0; i < n; i++)
      cout << A[i] << " ";
    cout << endl;
  } else for (int i = 1; i <= n; i++) {//從小到大依次考慮S中的每一個元素,S中的元素表示集合{1..n}除A以外的元素
    int ok = 1;
    for (int j = 0; j < cur; j++)
      if (A[j] == i) ok = 0; //表示i在A中出現過,那麼就不考慮該元素了
    if (ok) {
      A[cur] = i;
      print_emu(A, n, cur+1);
    }
  }
}
int main() {
  int x[5];
 print_emu(A, 5, 0);
 return 0;
}

利用以上兩個全排列模板,輕鬆KO掉poj1015

利用模板一的代碼

#include <iostream>
using namespace std;


char x[12];
char s[13];
int n;
bool flag = false;
void swap(int &a,int &b) {
  int tmp =a;
  a = b;
  b = tmp;
}

int sqr(char a, int n) {
  if (n == 1)
    return a - 'A' + 1;
  return (a - 'A' + 1)* sqr(a, n - 1);
}
void Backtrack(int t) {
  int i;
  if(t == 5) {
    if (sqr(x[0], 1) - sqr(x[1], 2) + sqr(x[2], 3)- sqr(x[3], 4) + sqr(x[4], 5) == n) {
      flag = true;
      for (int i = 0; i < 5;i++)
        cout << x[i];
      cout << endl;
    }
    return;
  }
  if (flag) //表示已經有結果了
    return;
  for(i = t;i < strlen(s);i++) {
    swap(s[t],s[i]);
    x[t] = s[t];
    Backtrack(t+1);
    swap(s[i],s[t]);
  }
}

int cmp(const void *a, const void *b) {
  return *((char*)b) - *((char*)a);
}
int main() {
while (cin >> n >> s && !(n == 0 && strcmp(s, "END") == 0)) {
  flag = false;
  qsort(s, strlen(s), sizeof(s[0]), cmp);
  Backtrack(0);
  if (!flag) {
    cout << "no solution" << endl;
  }
}
  return 0;
}


利用模板二的代碼

①x數組保存的是a數組的索引

#include <iostream>
using namespace std;


int sqr(char a, int n) {
  if (n == 1)
    return a - 'A' + 1;
  return (a - 'A' + 1)* sqr(a, n - 1);
}
int cmp(const void *a, const void *b) {
  return *((char*)b) - *((char*)a);
}

bool print_emu(char a[], int x[], int n, int cur, int target) {
  if (cur == 5) {
    if (sqr(a[x[0] - 1], 1) - sqr(a[x[1]-1], 2) + sqr(a[x[2]-1], 3) - sqr(a[x[3]-1], 4) + sqr(a[x[4]-1], 5) == target)
      return true;
  } else for (int i = 1; i <= n; i++) {
    int ok = 1;
    for (int j = 0; j < cur; j++)
      if (x[j] == i) ok = 0;
    if (ok) {
      x[cur] = i;
      if (print_emu(a, x, n, cur+1, target))
        return true;
    }
  }
  return false;
}

int main() {
  int x[13], target;
  char a[13];
  while (cin >> target >> a && !(target == 0 && strcmp(a, "END") == 0)) {
    qsort(a, strlen(a), sizeof(a[0]), cmp);
    if (print_emu(a, x, strlen(a), 0, target)) {
      for (int i = 0; i < 5; i++)
        cout << a[x[i]-1];
      cout << endl;
    } else {
      cout << "no solution" << endl;
    }
  }
  return 0;
}

②x數組保存的是a數組中的字符串

#include <iostream>
using namespace std;


int sqr(char a, int n) {
  if (n == 1)
    return a - 'A' + 1;
  return (a - 'A' + 1)* sqr(a, n - 1);
}
int cmp(const void *a, const void *b) {
  return *((char*)b) - *((char*)a);
}

bool print_emu(char a[], char x[], int n, int cur, int target) {
  if (cur == 5) {
    if (sqr(x[0], 1) - sqr(x[1], 2) + sqr(x[2], 3) - sqr(x[3], 4) + sqr(x[4], 5) == target)
      return true;
  } else for (int i = 0; i < n; i++) {
    int ok = 1;
    for (int j = 0; j < cur; j++)
      if (x[j] == a[i]) ok = 0;
    if (ok) {
      x[cur] = a[i];
      if (print_emu(a, x, n, cur+1, target))
        return true;
    }
  }
  return false;
}

int main() {
  char x[13];
  int target;
  char a[13];
  while (cin >> target >> a && !(target == 0 && strcmp(a, "END") == 0)) {
    qsort(a, strlen(a), sizeof(a[0]), cmp);
    if (print_emu(a, x, strlen(a), 0, target)) {
      for (int i = 0; i < 5; i++)
        cout << x[i];
      cout << endl;
    } else {
      cout << "no solution" << endl;
    }
  }
  return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章