combinatorial search

55
1 Combinatorial Search

Upload: marcia-barker

Post on 31-Dec-2015

30 views

Category:

Documents


7 download

DESCRIPTION

Combinatorial Search. 由于现代计算机的超高速度,搜索法已经成为了一个解决问题的有效途径。搜索被称为 “ 通用解题法 ” ,在算法和人工智能中占有重要地位。 现代的个人计算机时钟频率为 GHz 级,即每秒运算 10 亿条指令。由于大多数有用的操作需要数百条指令(甚至更多),所以每秒能搜索几百万个元素就不错了。 一百万种排列约是 10 个元素的所有排列总数。一百万种子集约是 20 个元素的所有组合。 如果需要求解更大规模问题,我们需要在搜索中剪枝,以确保只会搜索到有必要搜索的元素。. Contents. 基础知识 广度优先搜索 深度优先搜索(回溯法) - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Combinatorial Search

1

Combinatorial Search

Page 2: Combinatorial Search

2

由于现代计算机的超高速度,搜索法已经成为了一个解决问题的有效途径。搜索被称为“通用解题法”,在算法和人工智能中占有重要地位。

现代的个人计算机时钟频率为 GHz 级,即每秒运算 10 亿条指令。由于大多数有用的操作需要数百条指令(甚至更多),所以每秒能搜索几百万个元素就不错了。

一百万种排列约是 10 个元素的所有排列总数。一百万种子集约是 20 个元素的所有组合。

如果需要求解更大规模问题,我们需要在搜索中剪枝,以确保只会搜索到有必要搜索的元素。

Page 3: Combinatorial Search

3

Contents 基础知识

广度优先搜索 深度优先搜索(回溯法)

例题 构造所有子集 构造所有排列 八皇后问题 马的周游 魔板

Page 4: Combinatorial Search

4

问题的状态空间

• 状态是对问题在某一时刻的进展情况的数学描述。• 搜索的过程实际上是遍历一个隐式图,图的顶点对

应着状态,有向边对应于状态转移,这个图称为状态空间。

• 一个可行解就是一条从起始状态对应顶到出发到目标状态集中任意一个顶点的路径。

Page 5: Combinatorial Search

5

问题的状态空间• 一个问题的解就是目标状态。• 解向量:一个问题的解往往能够表示成一个 n 元

组 (x1,x2,…,xn) 的形式。• 显约束:对分量 xi 的取值限定。• 隐约束:为满足问题的解而对不同分量之间施加

的约束。• 解空间:对于问题的一个实例,解向量满足显式

约束条件的所有多元组,构成了该实例的一个解空间。

注意:同一个问题可以有多种表示,有些表示方法更简单,所需表示的状态空间更小(存储量少,搜索方法简单)。

Page 6: Combinatorial Search

6

0-1 背包问题给定 n 种物品和一背包。物品 i 的重量为 wi ,其价值为 vi ,背包的容量为 c 。问应如何选择装入背包中的物品,使得装入背包中的物品的总价值最大?实例:n=3, w[ ]={16, 15, 15}, p[ ]={45, 25, 25}, c=30 。任一状态可表示为 (x1,x2,x3). xi 为 1 或者 0 ,表示是否选取物品 i 。初始状态 (0,0,0) 。

Page 7: Combinatorial Search

7

旅行商问题某商人要到若干城市去推销商品,已知各城市之间的路程。他要选一条从驻地出发,经过每个城市一次,最后回到驻地的路线,使总的路程最小。图论建模:设 G=(V,E) 是一个带权图。图中各边的费用(权)为正数。图中的一条周游路线是包括 V 中的每个顶点在内的一条回路。一条周游路线的费用是这条路线上所有边的费用之和。如何在图 G 中找出一条有最小费用的周游路线?

Page 8: Combinatorial Search

8

子集树与排列树

遍历子集树需 O(2n) 计算时间 遍历排列树需要 O(n!) 计算时间

Page 9: Combinatorial Search

9

生成问题状态的基本方法 扩展结点 : 一个正在产生儿子的结点称为扩展结点 活结点 : 一个自身已生成但其儿子还没有全部生成

的节点称做活结点 死结点 : 一个所有儿子已经产生的结点称做死结点 宽度优先的问题状态生成法:在一个扩展结点变成

死结点之前,它一直是扩展结点。 深度优先的问题状态生成法:如果对一个扩展结点R ,一旦产生了它的一个儿子 C ,就把 C 当做新的扩展结点。在完成对子树 C (以 C 为根的子树)的穷尽搜索之后,将 R 重新变成扩展结点,继续生成R 的下一个儿子(如果存在)。

Page 10: Combinatorial Search

10

回溯法 有许多问题,当需要找出它的解集或者要求

回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。

回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。

Page 11: Combinatorial Search

11

回溯法的基本思想(1)针对所给问题,定义问题的解空间;(2)确定易于搜索的解空间结构;(3) 以深度优先方式搜索解空间,并在搜索过程

中用剪枝函数避免无效搜索。

用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为 h ,则回溯法所需的计算空间通常为 O(h) 。而显式地存储整个解空间则需要O(2h) 或 O(h!) 内存空间。

Page 12: Combinatorial Search

12

生成 1~n 的排列要求:按照字典顺序输出 1~n 的排列。

Void print_permutation( 序列 A, 集合 S){ if (S 为空 ) 输出序列 A; else 按照从小到大的顺序依次考虑 S 中的每个元素 v { print_permutation( 在 A 的末尾添加 v 后得到的新序列 ,

S – {v}); }}

Page 13: Combinatorial Search

生成 1~n 的排列void print_permutation(int n, int* A, int cur) { int i, j; if(cur == n) { // 递归边界 for(i = 0; i < n; i++) printf("%d ", A[i]); printf("\n"); } else for(i = 1; i <= n; i++) {

// 尝试在 A[cur] 中填各种整数 i int ok = 1; for(j = 0; j < cur; j++) if(A[j] == i) ok = 0;

// 如果 i 已经在 A[0]~A[cur-1] 出现过,则不能再选 if(ok) { A[cur] = i; print_permutation(n, A, cur+1); // 递归调用 } }}

调用方式 print_permutation(n, A, 0);

Page 14: Combinatorial Search

14

生成可重集的排列// 输出数组 P 中元素的全排列。数组 P 中可能有重复元素, P 已排序void print_permutation(int n, int* P, int* A, int cur){ int i, j; if(cur == n) { for(i = 0; i < n; i++) printf("%d ", A[i]); printf("\n"); } else for(i = 0; i < n; i++) if(!i || P[i] != P[i-

1]) {

int c1 = 0, c2 = 0; for(j = 0; j < cur; j++) if(A[j] == P[i]) c1++; for(j = 0; j < n; j++) if(P[i] == P[j]) c2++; if(c1 < c2) { A[cur] = P[i]; print_permutation(n, P, A, cur+1); } }}

Page 15: Combinatorial Search

15

下一个排列// 从字典序最小排列开始,不停“求下一个排列”。// 适用于可重集。#include<cstdio>#include<algorithm>using namespace std;int main() { int n, p[10]; scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%d", &p[i]); sort(p, p+n); // 排序,得到 p 的最小排列 do {// 输出排列 p for(int i = 0; i < n; i++) printf("%d ", p[i]);

printf("\n"); } while(next_permutation(p, p+n)); // 求下一个排列 return 0;}

Page 16: Combinatorial Search

16

子集生成

There are 2n subsets of an n-element set, say the integers {1,2,…,n}.

We set up an vector A[a1,a2,…,an], where the value ai (true or false) signifies whether the ith item is in the given subset.

Page 17: Combinatorial Search

17

子集生成 -增量构造法void print_subset(int n, int* A, int cur) { for(int i = 0; i < cur; i++) printf("%d ", A[i]); // 打印当前集合

printf("\n"); // 确定当前元素的最小可能值 int s = cur ? A[cur-1]+1 : 0; for(int i = s; i < n; i++) { A[cur] = i; print_subset(n, A, cur+1); // 递归构造子

集 }}int main() { int A[10]; print_subset(5, A, 0);}

Page 18: Combinatorial Search

18

子集生成 -位向量法位向量 B[i]=1 当且仅当 i 在子集 A 中。void print_subset(int n, int* B, int cur) { if(cur == n) { for(int i = 0; i < cur; i++) if(B[i]) printf("%d ", i); // 打印当前集

合 printf("\n"); return; } B[cur] = 1; // 选第 cur 个元素 print_subset(n, B, cur+1); B[cur] = 0; // 不选第 cur 个元素 print_subset(n, B, cur+1);}调用: int B[10]; print_subset(5, B, 0);

Page 19: Combinatorial Search

子集生成 -二进制法

19

可以用二进制来表示 {0,1,2,…,n-1} 的子集 S: 从右到左的第 i 位(从 0 开始编号)表示元素 i 是否出现在集合 S 中。

位运算与集合运算

A&B: 集合交; A|B: 集合并; A^B: 集合对称差空集为 0 ,全集 {0,1,2,…,n-1} 为 2n-1 ,即 (1<<n)-1集合 A 的补集为 A ^ ((1<<n)-1)

A B A&B A|B A^B

二进制 10110 01100 00100 11110 11010

集合 {1,2,4} {2,3} {2} {1,2,3,4} {1,3,4}

Page 20: Combinatorial Search

子集生成 -二进制法

20

// 打印 {0,1,2,…,n-1} 的子集 svoid print_subset(int n, int s){ for(int i=0;i<n;i++) if (s&(1<<i)) printf(“%d ”,i); printf(“\n”);}// 枚举子集// 枚举各子集所对应的编码 0,1,2,…,2n-1for(int i=0; i< (1<<n); i++) print_subset(n,i);

Page 21: Combinatorial Search

回溯法

21

• 在递归构造过程中,生成和检查过程可以有机结合起来,从而减少不必要的枚举。

Page 22: Combinatorial Search

22

N Queens Problem在 n×n格的棋盘上放置彼此不受攻击的 n 个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 后问题等价于在 n×n格的棋盘上放置 n 个皇后,任何 2 个皇后不放在同一行或同一列或同一斜线上。

1 2 3 4 5 6 7 8

1

2

345

678

Q

QQ

Q

QQ

Q

Q

Page 23: Combinatorial Search

23

• 应该如何合理地表示 N 皇后问题的解?这个解有多大?

• 方法 1 :转化为子集枚举问题。• 从 64 个格子中选一个子集,使得子集中恰好有 8 个格子,且任意两个选出的格子都不在同一行、同一列或同一个对角线上。

• 这种表示法的代价太高。对于 8×8 的棋盘,有 264=1.84×1019 个子集。

N Queens Problem

Page 24: Combinatorial Search

24

• 应该如何合理地表示 N 皇后问题的解?这个解有多大?

• 方法 2 :用解向量的第 i 个元素显式的给第 i 个皇后的格子编号。在这种表示法下 ai 是一个 1 到 n2

之间的整数,当且仅当所有 n 个位置被赋值时 a 对应一个合法解。第 i 个位置的候选集为前 i-1 个皇后没有攻击到的所有格子。

• 方法 2 分析:对于 8×8 的棋盘,约有648=2.81×1014 个可能的解向量。这离通常所能承受的最大搜索空间大小(百万级)仍然相距甚远。

N Queens Problem

Page 25: Combinatorial Search

25

• 应该如何合理地表示 N 皇后问题的解?这个解有多大?

• 方法 3 : N 皇后问题的合法解中,每行恰好有一个皇后,这样第 i 个皇后的位置只能在第 i 行的 8 个格子中,搜索空间大小缩小到 88=1.677×107 。

• 方法 4 :进一步,由于任意两个皇后所在的列也应互不相同,合法解中各个皇后所在的列一定是这 N列的一个排列。这样,搜索空间大小只剩 8!=40320 。实现时用 C[x] 表示第 x 行皇后的列编号。

N Queens Problem

Page 26: Combinatorial Search

N Queens Problemint C[50], tot = 0, n = 8, nc = 0;void search(int cur) { int i, j; nc++; if(cur == n) // 递归边界 { tot++; } else for(i = 0; i < n; i++) { int ok = 1; C[cur] = i;// 尝试把第 cur 行皇后放在第 i 列 for(j = 0; j < cur; j++)// 检查是否和前面的皇后冲突 if(C[cur] == C[j] || cur-C[cur] == j-C[j] ||

cur+C[cur] == j+C[j]) { ok = 0; break; } if(ok) search(cur+1); }}

调用: search(0); printf("%d\n", tot); printf("%d\n", nc);

Page 27: Combinatorial Search

27

•我们的实现可以计算多大规模的 N 皇后问题?• 如果不是需要求出 N 皇后问题的所有解,只需

要找到一个可行解,有没有其他高效算法?

N Queens Problem

Page 28: Combinatorial Search

28

Search Pruning Pruning is the technique of cutting off the

search the instant we have established that a partial solution cannot be extended into a full solution.

For traveling salesman, we seek the cheapest tour that visit all vertices. Suppose that in the course of our search we find a tour t whose cost is Ct. Later, we may have a partial solution a whose edge sum CA>Ct. Is there any reason to continue exploring this node? No!

Page 29: Combinatorial Search

292011-10-20

1152 1153 马周游

题目大意 : 一个有限大小的棋盘上有一只马,马只能按日字方式走,如图所示。

给出初始时马的位置,找出一条马移动的路线,经过所有格子各一次。

Page 30: Combinatorial Search

302011-10-20

1152 1153 马周游

解题思路: 枚举马能走的所有路径,直至找到一条完成周

游的路径; 递归,回溯。

Page 31: Combinatorial Search

31

1152 1153 马周游bool solve(int x, int y, int lev) {

route[lev] = x * N + y;if (lev == M * N - 1) {print_route();return true;}visited[x][y] = true;grid grids[8];int n = get_grid(grids,x,y);for (i=0; i<n; i++)

if (solve(grids[i].x, grids[i].y, lev+1)) return true;

visited[x][y] = false;return false;

}

Page 32: Combinatorial Search

32

1152 1153 马周游int get_grid(grid grids[], int x,int y) { int n=0;

for (int i=0; i<8; i++) { int xx = x + direction[i][0];

int yy = y + direction[i][1]; if (xx>=0&&yy>=0&&xx<M&&yy<N&&!visited[xx][yy])

{ grids[n].x = xx; grids[n].y = yy; n++; }}return n;

}

Page 33: Combinatorial Search

332011-10-20

1152 1153 马周游

以上程序速度过慢。 优化:改变搜索顺序。

优先搜索可行格较少的格子。 其他顺序。

修改 get_grid()函数。

Page 34: Combinatorial Search

34

1152 1153 马周游int get_grid(grid grids[], int x,int y) {

int n=0;for (int i=0; i<8; i++) { int xx = x + direction[i][0] int yy = y + direction[i][1]; if (xx>=0&&yy>=0&&xx<M&&yy<N&&!visited[xx][yy])

{grids[n].x = xx; grids[n].y = yy;grids[n].count = get_count(xx, yy);n++;

}}sort(grids,grids+n);return n;

}

Page 35: Combinatorial Search

35

1152 1153 马周游bool operator < (const grid &a, const grid &b) {

return a.count < b.count;}

int get_count(int x, int y) {int i, xx, yy, count = 0;for (i=0; i<8; i++) { xx = x + direction[i][0];

yy = y + direction[i][1]; if (xx>=0&&yy>=0&&xx<M&&yy<N&&!visited[xx][yy])

count++;}return count;

}

Page 36: Combinatorial Search

1152 简单的马周游问题

隐藏算法:

5*6 规模比较小 仅 30 种输入 每种的输出仅 30 个整数 完全可以使用 const 大法 先在本机跑出所有结果,然后 O(1) 输出

const

Page 37: Combinatorial Search

1050 Numbers & Letters 题目大意

给 5 个整数,使用 +, -, *, / 四种运算,可任意安排顺序和加括号,求一个不超过某给定值的最优解

解题思路 Dfs(S) //S 为操作数的集合 从 S 中任取两个数 a, b 进行运算得 c dfs(S-{a,b}+{c}) 复杂度:

C(5,2)*4*C(4,2)*4*C(3,2)*4*C(2,2)*4 = 46080

Page 38: Combinatorial Search

1050 Numbers & Lettersvoid dfs(int a[], int n) { if (n==1) return; int b[5],m=0; for(int i=0;i<n;i++) for(int j=i+i;j<n;j++) { for(int k=0;k<n;k++) if (k!=i && k!=j) b[m++]=a[k]; update_answer(b[m]=a[i]+a[j]); dfs(b,m+1); update_answer(b[m]=a[i]-a[j]); dfs(b,m+1); update_answer(b[m]=a[j]-a[i]); dfs(b,m+1); update_answer(b[m]=a[i]*a[j]); dfs(b,m+1); if (a[j]!=0 && a[i]%a[j]==0) { update_answer(b[m]=a[i]/a[j]); dfs(b,m+1); } if (a[i]!=0 && a[j]%a[i]==0) { update_answer(b[m]=a[j]/a[i]); dfs(b,m+1); } }}

Page 39: Combinatorial Search

392011-10-12

1006 Team Rankings

题目大意: 对于两个排列 p,  q ,定义 distance( p, q )

为在 p,  q 中出现的相对次序不同的元素的对数。相当于以 p 为基准,求 q 的逆序数。

给出 n 个 5 元排列,构造一个排列,使得该排列对 n 个排列的 distance 之和最小。

n<=100

Page 40: Combinatorial Search

402011-10-12

1006 Team Rankings

解题思路: 枚举所有 5 元排列,与 n 个排列一一比较 5 个

元素之间顺序并累加; 枚举方法可用递归。 求逆序数的算法

平方级枚举 n^2 规模较大时可采用归并排序 nlogn

Page 41: Combinatorial Search

1006 Team Rankingsvoid dfs(char rank[],int lev) {

if (lev==5) {rank[5]='\0';cal(rank);return;

}for (char c='A';c<='E';c++)

if (!used[c]) {rank[lev]=c;used[c]=true;dfs(rank,lev+1);used[c]=false;

}}

Page 42: Combinatorial Search

1006 Team Rankings

void cal(char rank[]) {

int count=0;for (int i=0;i<n;i++)

count+=distance(ranks[i],rank);if (count<ans_count) {

ans_count=count;strcpy(ans,rank);

}}

Page 43: Combinatorial Search

1006 Team Rankings

int distance(char a[],char b[]) { int cnt=0; for (int i=0;i<5;i++) { posa[a[i]]=i; posb[b[i]]=i; } for (char c1='A';c1<='E';c1++) for (char c2=c1+1;c2<='E';c2++) if ((posa[c1]-posa[c2])*(posb[c1]-posb[c2])<0) cnt++; return cnt;}

Page 44: Combinatorial Search

Tips What is 求逆序数的快速算法 ? 归并排序的原理

What is nlogn? 通常出现的算法复杂度级别 O(mn),O(n!),O(nm),O(n),O(logn),O(1) 当 n>10000 时,至少要 O(nlogn)

Page 45: Combinatorial Search

1150 1151 1515 魔板 题目大意

魔板是 2 行 4 列的方格,八格分别标为 1-8 初始状态为 1 2 3 4

8 7 6 5 有三种操作:

上下两行互换

1 2 3 4

5678

Page 46: Combinatorial Search

1150 1151 1515 魔板 题目大意

魔板是 2 行 4 列的方格,八格分别标为 1-8 初始状态为 1 2 3 4

8 7 6 5 有三种操作:

上下两行互换 每行循环右移一格

1 2 3 4

5678

Page 47: Combinatorial Search

1150 1151 1515 魔板 题目大意

魔板是 2 行 4 列的方格,八格分别标为 1-8 初始状态为 1 2 3 4

8 7 6 5 有三种操作:

上下两行互换 每行循环右移一格 中间四块顺时针转一格

1 2 3 4

5678

Page 48: Combinatorial Search

1150 1151 1515 魔板 题目大意

魔板是 2 行 4 列的方格,八格分别标为 1-8 初始状态为 1 2 3 4

8 7 6 5 有三种操作:

上下两行互换 每行循环右移一格 中间四块顺时针转一格

给定一个终止状态,求最小操作数及方案

1 2 3 4

5678

1

2

4

5 67

83

Page 49: Combinatorial Search

1150 1151 1515 魔板

解题思路

对模板进行状态搜索

由一种状态可以转移到另外三种状态,搜索树为一棵三叉树

在这棵三叉树上搜索,目的是求出最优解

Page 50: Combinatorial Search

1150 1151 1515 魔板

算法一:盲目 DFS

对这棵三叉树进行 DFS 若想求得最优解,需要遍历整棵树 需要进行重复扩展 优化: 若已找到一个可行解,可剪去大于等于这个深度的

所有子树

评价:效果: 加优化后勉强可过 1150

很傻很天真

Page 51: Combinatorial Search

1150 1151 1515 魔板

算法二: BFS

对这棵三叉树进行 BFS 第一个可行解即是最优解

评价:效果:轻松切掉 1150 ,但过不了

1151很慢很暴力

Page 52: Combinatorial Search

1150 1151 1515 魔板

算法三: DFS/BFS + 判重 可将一个魔板的状态编码,用 8!以内的整数表示

一个状态 可用 8!的数组判重 不管 DFS还是 BFS ,最多搜 8! =40320 个状态

评价:效果:轻松切掉以上三题

很快很易写

Page 53: Combinatorial Search

532011-10-12

1150 1151 1515 魔板

算法三: BFS + 判重 对这棵三叉树进行 BFS ,相同的节点不再重复扩展 第一个可行解即是最优解 判重: BFS 每经过一个节点,把它放进已搜索列表中,

每遇到一个节点,如果在已搜索列表存在,则不再扩展该节点,共 8!=40320 个节点。

判重编码:康托展开、自定义编码,如初始状态可编码为整数 12348765 。

可以轻松通过三个题目。

Page 54: Combinatorial Search

542011-10-12

1150 1151 1515 魔板

state q[40320]; void update(state new_state,int &head,int

&tail, char opt) { q[head]=new_state; visit[hash(new_state)]=true; parent[head]=tail; op[head++]=opt; }

Page 55: Combinatorial Search

552011-10-12

1150 1151 1515 魔板 void bfs(state start) { int head=0,tail=0; update(start,head,tail,'\0'); while (tail<head) { state new_state=A(q[tail]); if (visit[hash(new_state)]==false) update(new_state,head,tail,'A') new_state=B(q[tail]); if (visit[hash(new_state)]==false) update(new_state,head,tail,'B') state new_state=C(q[tail]); if (visit[hash(new_state)]==false) update(new_state,head,tail,'C') tail++; } }