Hướng dẫn quay lui trong python

Hướng dẫn quay lui trong python

Đã đăng vào thg 7 27, 2017 4:42 CH 4 phút đọc

Quay lui là một kĩ thuật thiết kế giải thuật dựa trên đệ quy. Ý tưởng của quay lui là tìm lời giải từng bước, mỗi bước chọn một trong số các lựa chọn khả dĩ và đệ quy. Người đầu tiên đề ra thuật ngữ này (backtrack) là nhà toán học người Mỹ D. H. Lehmer vào những năm 1950.

Tư tưởng

Dùng để giải bài toán liệt kê các cấu hình. Mỗi cấu hình được xây dựng bằng từng phần tử. Mỗi phần tử lại được chọn bằng cách thử tất cả các khả năng.

Các bước trong việc liệt kê cấu hình dạng X[1...n]:

  • Xét tất cả các giá trị X[1] có thể nhận, thử X[1] nhận các giá trị đó. Với mỗi giá trị của X[1] ta sẽ:
  • Xét tất cả giá trị X[2] có thể nhận, lại thử X[2] cho các giá trị đó. Với mỗi giá trị X[2] lại xét khả năng giá trị của X[3]...tiếp tục như vậy cho tới bước:
  • ...
  • ....
  • Xét tất cả giá trị X[n] có thể nhận, thử cho X[n] nhận lần lượt giá trị đó.
  • Thông báo cấu hình tìm được.

Bản chất của quay lui là một quá trình tìm kiếm theo chiều sâu(Depth-First Search).

Mô hình thuật toán

  • Mã giả cho thuật toán quay lui.
Backtracking(k) {
	for([Mỗi phương án chọn i(thuộc tập D)]) {
		if ([Chấp nhận i]) {
			[Chọn i cho X[k]];
			if ([Thành công]) {
				[Đưa ra kết quả];
			} else {
				Backtracking(k+1);
				[Bỏ chọn i cho X[k]];
			}
		}
	}
}

Ví dụ: Trò chơi Sudoku

Sudoku là một trò chơi khá phổ biến và chắc ai cũng biết. Trò chơi như sau: có một hình vuông được chia thành 9x9 ô vuông con. Mỗi ô vuông con có giá trị trong khoảng từ 1 đến 9. Ban đầu hình vuông có một số ô vuông con cho trước (có điền sẵn số) và còn lại là trống. Hãy điền các số từ 1-9 vào các ô con lại sao cho: hàng ngang là các số khác nhau từ 1 đến 9, hàng dọc là các số khác nhau từ 1 đến 9, và mỗi khối 3x3 chính là các số khác nhau từ 1 đến 9. Sau đây là 1 ví dụ về đề bài và lời giải:

Hướng dẫn quay lui trong python

Áp dụng quay lui để giải bài toán sudoku. Ý tưởng: Mỗi bước tìm tập các giá trị khả dĩ để điền vào ô trống, và sau đó đệ quy để điền ô tiếp theo. Giả mã của thuật toán (ở đây chú ý mảng chỉ có kích thước 9×9×9)

void solveSudoku(int S[][9], int x, int y){
    if(y == 9){
        if(x == 8){
            printSolution(S);
            exit(0);
        } else {
            solveSudoku(S, x+1,0);
        }
    } else if(S[x][y] == 0){
        for (int k = 1; k <=9; k++){
            if(checkValid(S,x,y,k)){
                S[x][y] = k;
                solveSudoku(S, x, y+1);
                S[x][y] = 0;
            }
        }
    } else {
        solveSudoku(S,x,y+1);
    }
}

boolean checkValid(int S[][9], int x, int y, int k){
    for(int i = 0; i <9 ; i++){
        if(S[x][i] == k) return false;
    }
    for(int i = 0; i <9 ; i++){
        if(S[i][y] == k) return false;
    }
    int a = x/3, b = y/3;
    for(int i = 3*a; i < 3*a+3; i++){
        for(int j = 3*b; j < 3*b+3; j++){
            if(S[i][j] == k) return false;
        }
    }
    return true;
}

Nhận xét

  • -Ưu điểm: Việc quay lui là thử tất cả các tổ hợp để tìm được một lời giải. Thế mạnh của phương pháp này là nhiều cài đặt tránh được việc phải thử nhiều trường hợp chưa hoàn chỉnh, nhờ đó giảm thời gian chạy.

  • Nhược điểm: Trong trường hợp xấu nhất độ phức tạp của quay lui vẫn là cấp số mũ. Vì nó mắc phải các nhược điểm sau:

    • Rơi vào tình trạng "thrashing": qúa trình tìm kiếm cứ gặp phải bế tắc với cùng một nguyên nhân.
    • Thực hiện các công việc dư thừa: Mỗi lần chúng ta quay lui, chúng ta cần phải đánh giá lại lời giải trong khi đôi lúc điều đó không cần thiết.
    • Không sớm phát hiện được các khả năng bị bế tắc trong tương lai. Quay lui chuẩn, không có cơ chế nhìn về tương lai để nhận biết đc nhánh tìm kiếm sẽ đi vào bế tắc.

All rights reserved

Ngôn ngữ Python cho phép hàm gọi đến chính nó, người ta gọi phương pháp này là phương pháp đệ quy hoặc quay lui (xem thêm bài viết Đệ quy và giải thuật đệ quy).

Cú pháp:

def  tên_hàm(danh_sách_tham_số){

if(điều_kiện_dừng_thỏa)

return giá_trị;

else

return tên_hàm(danh_sách_đối_số) phép_toán tên_đối_số;

}

* Trong giải thuật này phải có một điều_kiện_dừng_thỏa để đệ quy kết thúc (dừng quay lui).

phép_toán ở đây là một phép toán bất kỳ phù hợp với bài toán của bạn.

* Chương trình sử dụng đệ quy thì dễ hiểu nhưng hao tốn tài nguyên CPU, dẫn đến làm giảm thời gian chạy chương trình đi nhiều nếu số lần đệ quy của hàm lớn.

Ví dụ 1:

Tính tổng các số chia hết cho 5 nằm trong đoạn [0,N] với N được nhập từ bàn phím.

Phân tích: Điều kiện dừng thỏa ở đây ta có thể thấy được ngay là khi giá trị của đối số bằng 0 (nếu ta chạy ngược giảm dần từ N) hoặc bằng N (nếu ta chạy xuôi tăng dần từ 0). Vì chỉ tính tổng các số chia hết cho 5 nên cứ mỗi lần đệ quy thì ta lại giảm (hoặc tăng) giá trị của đối số 5 đơn vị rồi tiến hành cộng dồn.

Chương trình được viết như sau:

def tinhTong(N):
  if N<=0:  #nếu N==0
    return 0; #thì dng đquy
  else:     #nếu không thì
    return tinhTong(N-5) + N; #tiếp tc đquy

def main():
  #tiến hành nhp N
  N=int(input("Mi nhp N: "))
  while N<1 or N%5!=0: #N phi là t1 trlên và chia hết cho 5 mi hp l    N=int(input("Mi nhp N: "))
  print("Tng các s t 1 đến",N,"chia hết cho 5 là:", tinhTong(N)) #tiến hành đquy

main()

Kết quả:

Mời nhập N: 100
Tổng các số từ 1 đến 100 chia hết cho 5 là: 1050

Ví dụ 2:

Ta có thể lập trình tính giai thừa theo phương pháp đệ quy, vì n!=1*2*3*…(n-1)*n  hay n!=n*(n-1)*(n-2)*…*1 --> Giá trị sau chính là giá trị trước cộng thêm 1 (hoặc là giá trị trước trừ đi 1), giá trị kết thúc =1.

Sau đây là phần demo:

def giaiThua(n):
  if n==0:    #điu kin dng tha
    return 1; #vì 0!=1 nên n==0 là vtrí kết thúc ca đquy
  return n*giaiThua(n - 1); #kiu n*(n-1)*(n-2)…*1
              #hoc: giaiThua(n-1)*n vi kiu 1*2*3*…*n

def main():
  n=int(input("Nhp n: "))
  print("Giai tha ca",n,"là", giaiThua(n));

main()

Ví dụ 3:

In ra n phần tử đầu tiên của dãy Fibonanci (1 1 2 3 5 8 13 21 34 …).

Phân tích: Hai phần tử đầu tiên của dãy là hai phần tử khởi tạo (1 1), bắt đầu từ phần tử thứ ba trở đi sẽ tuân theo quy tắc là phần tử sau bằng tổng của hai phần tử ngay trước nó cộng lại (ví dụ 13=5+8). Công thức: n= (n-1) + (n-2). Như vậy có nghĩa ta sẽ sử dụng được phương pháp đệ quy vì nó tuân theo cú pháp đệ quy.

Chương trình được viết như sau:

def fibo(n):
  if(n==0 or n==1): #nếu là hai phn tđu tiên ca dãy (điu kin dng tha)
    return 1;       #thì scó giá tr1
  return fibo(n-1) + fibo(n-2); #nếu không thì tính giá trca phn tđó

def main():
  n=int(input("Nhp s phn t cn xem ca dãy Fibonacci: "))
  print(n,"phn t đu tiên ca dãy là:");
  for i in range(0,n):
    print(fibo(i));

main()

Kết quả:

Nhập số phần tử cần xem của dãy Fibonacci: 10
10 phần tử đầu tiên của dãy là:
1
1
2
3
5
8
13
21
34
55