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

假设现在有一列数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 
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 
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 
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 
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 
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 
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;
}


你可能感兴趣的:(数据结构算法,c++)