Trăn diện tích hình thang

Đệ quy dựa trên một quan sát đơn giản, mà tôi sẽ đưa ra một đối số tổ hợp, về lý do tại sao nó đúng, thay vì chứng minh toán học thông qua các công thức

Nội dung chính Hiển thị

  • Chọn phần tử #n
  • Không chọn phần tử #n
  • trường hợp cơ sở
  • Đảm bảo nó hoạt động
  • kết hợp
  • Ghi nhớ kết quả
  • Thay thế đệ quy bằng một bảng
  • hội nhập Romberg
  • Bài tập lập trình

Bất cứ khi nào bạn chọn

    subset[n - 1, k]
9 phần tử trong số
if k == 0:
    return 1
if n == k:
    return 1
0, sẽ có hai trường hợp xảy ra

  1. Bạn chọn yếu tố
    if k == 0:
        return 1
    if n == k:
        return 1
    
    1
  2. Bạn không chọn yếu tố
    if k == 0:
        return 1
    if n == k:
        return 1
    
    1

Vì các sự kiện này loại trừ lẫn nhau nên tổng số kết hợp được tính bằng số lượng kết hợp khi chọn

if k == 0:
    return 1
if n == k:
    return 1
1 và số kết hợp khi bạn không chọn
if k == 0:
    return 1
if n == k:
    return 1
1

Chọn phần tử #n

Vì chúng tôi đã chọn một phần tử, chúng tôi chỉ cần chọn một phần tử

if k == 0:
    return 1
if n == k:
    return 1
5 khác. Ngoài ra, vì chúng ta đã quyết định về một phần tử – là nó có được bao gồm hay không – rồi, nên chúng ta chỉ cần xem xét các phần tử còn lại của
if k == 0:
    return 1
if n == k:
    return 1
6

Do đó, số lượng kết hợp để chọn phần tử

if k == 0:
    return 1
if n == k:
    return 1
1 được đưa ra bởi

    subset[n - 1, k - 1]

Không chọn phần tử #n

Vẫn còn

    subset[n - 1, k]
9 phần tử để chọn, nhưng vì chúng ta đã quyết định về phần tử
if k == 0:
    return 1
if n == k:
    return 1
1, nên chỉ còn lại
if k == 0:
    return 1
if n == k:
    return 1
20 phần tử để chọn. Như vậy

    subset[n - 1, k]

trường hợp cơ sở

Đệ quy sử dụng thực tế là chúng ta thường có thể phân biệt giữa hai tình huống, giải pháp trong đó phần tử

if k == 0:
    return 1
if n == k:
    return 1
0 là một phần của giải pháp đó và những giải pháp không phải là

Tuy nhiên, không phải lúc nào cũng có thể phân biệt như vậy

  • Khi chọn tất cả các phần tử [tương ứng với trường hợp
    if k == 0:
        return 1
    if n == k:
        return 1
    
    22 trong mã bên dưới]
  • hoặc khi không chọn phần tử nào cả [tương ứng với trường hợp
    if k == 0:
        return 1
    if n == k:
        return 1
    
    23 trong mã bên dưới]

Trong những trường hợp này, chỉ có đúng một giải pháp, do đó

if k == 0:
    return 1
if n == k:
    return 1

Đảm bảo nó hoạt động

Để làm được điều đó, chúng ta cần thuyết phục bản thân [hoặc chứng minh] rằng trường hợp cơ bản luôn thành công vào một thời điểm nào đó

Giả sử rằng, tại một thời điểm nào đó,

if k == 0:
    return 1
if n == k:
    return 1
24. Vì theo giả định của chúng tôi,
if k == 0:
    return 1
if n == k:
    return 1
0 ban đầu lớn hơn hoặc bằng
    subset[n - 1, k]
9, nên chắc chắn phải có một điểm nào đó mà
if k == 0:
    return 1
if n == k:
    return 1
27, bởi vì
if k == 0:
    return 1
if n == k:
    return 1
0 và
    subset[n - 1, k]
9 giảm đồng thời hoặc chỉ
if k == 0:
    return 1
if n == k:
    return 1
0 giảm một, i. e. nó đi theo

Điều này ngụ ý rằng phải có một cuộc gọi đến

if k == 0:
    return 1
if n == k:
    return 1
51 để điều đó xảy ra, rằng
if k == 0:
    return 1
if n == k:
    return 1
0 giảm xuống dưới
    subset[n - 1, k]
9. Tuy nhiên, điều này là không thể vì chúng tôi có trường hợp cơ sở trên
if k == 0:
    return 1
if n == k:
    return 1
27 nơi chúng tôi trả về hằng số
if k == 0:
    return 1
if n == k:
    return 1
55

Chúng tôi kết luận rằng hoặc

if k == 0:
    return 1
if n == k:
    return 1
0 giảm tại một số điểm sao cho
if k == 0:
    return 1
if n == k:
    return 1
27 hoặc giảm đồng loạt chính xác
    subset[n - 1, k]
9 lần sao cho
if k == 0:
    return 1
if n == k:
    return 1
59

Vì vậy, trường hợp cơ sở hoạt động

kết hợp

Rất nhiều bài toán và khoa học máy tính có lời giải đệ quy. Trong phần này của bài giảng, chúng ta sẽ nghiên cứu một bài toán từ toán học có nghiệm đệ quy tao nhã

Ký hiệu kết hợp

hay "n chọn k" có nhiều ứng dụng trong toán học. Trong lý thuyết xác suất

đếm số cách mà chúng ta có thể có được k mặt ngửa trong một chuỗi n lần tung đồng xu công bằng

được tính theo công thức

Hai trường hợp đặc biệt dễ tính

Một cách để tính một tổ hợp là tính tất cả các giai thừa có liên quan rồi chia mẫu số cho tử số. Điều này không thực tế đối với giá trị n thậm chí lớn vừa phải vì các giai thừa tăng rất nhanh như một hàm của n. Thay vào đó, phương pháp phổ biến nhất để tính toán tổ hợp sử dụng mối quan hệ đệ quy này

Mối quan hệ đệ quy này kết hợp với hai trường hợp đặc biệt giúp có thể xây dựng định nghĩa hàm đệ quy trong Python để tính toán tổ hợp

if k == 0:
    return 1
if n == k:
    return 1
2

Điều này hoạt động và tính toán giá trị chính xác cho sự kết hợp

Cách tiếp cận này không có một lỗ hổng. Xem xét điều gì xảy ra khi chúng ta cố gắng tính C[8,5]

if k == 0:
    return 1
if n == k:
    return 1
5

Điều bạn bắt đầu nhận thấy sau một thời gian là các chức năng giống nhau được gọi nhiều lần. Hiệu ứng này trở nên nghiêm trọng đối với n và k lớn đến mức hàm đệ quy đơn giản được hiển thị ở trên trở nên vô cùng kém hiệu quả

Chúng ta có thể xây dựng một chương trình minh họa vấn đề này tồi tệ như thế nào. Thủ thuật là khai báo một mảng hai chiều toàn cầu

if k == 0:
    return 1
if n == k:
    return 1
6

lưu trữ thông tin về số lần chúng ta gọi C với mỗi cặp giá trị tham số n và k

Sau đó, chúng tôi sửa đổi hàm tính toán kết hợp để ghi lại số lần nó được gọi với mỗi cặp tham số

if k == 0:
    return 1
if n == k:
    return 1
7

Khi kết thúc chương trình, chúng tôi kết xuất dữ liệu đếm này vào một tệp

if k == 0:
    return 1
if n == k:
    return 1
8

Kết quả gây sốc. Đây là tập hợp các kết quả khi chúng tôi cố gắng tính toán C[16,10]

if k == 0:
    return 1
if n == k:
    return 1
9

Những gì chúng ta có thể đọc được từ bảng này là C[2,1], C[2,2] và C[3,2] mỗi cái được gọi tổng cộng 1287 lần trong quá trình tính toán C[16,10]. Vấn đề về các lệnh gọi hàm dư thừa này trở nên tồi tệ hơn theo cấp số nhân khi n và k lớn hơn. Vào thời điểm bạn đạt đến n = 30, phương pháp đệ quy đơn giản này để tính toán C[n,k] trở nên hoàn toàn không thực tế

Ghi nhớ kết quả

Cách khắc phục đơn giản cần thiết để loại bỏ các lệnh gọi dư thừa đến hàm đệ quy được gọi là ghi nhớ. Ý tưởng là ghi nhớ mọi giá trị mà chúng ta tính toán, để lần sau khi được yêu cầu tính toán một giá trị, chúng ta có thể tra cứu giá trị đó thay vì tính toán lại. Điều này hoạt động tốt nhất cho các hàm đệ quy với số tham số nguyên vừa phải. Trong trường hợp đó, chúng ta có thể nhớ các giá trị mà chúng ta đã tính toán trước đó bằng cách lưu trữ các giá trị trong một bảng. Bất cứ khi nào chúng tôi được yêu cầu tính toán một giá trị, chúng tôi bắt đầu bằng cách tham khảo bảng

Trong trường hợp chức năng tính toán các kết hợp, chúng tôi tiến hành như sau. Chúng tôi bắt đầu bằng cách tạo một mảng hai chiều toàn cầu để giữ các giá trị hàm đã nhớ

if k == 0:
    return 1
if n == k:
    return 1
0

Sau đó chúng tôi viết lại hàm đệ quy để sử dụng bảng. Bất cứ khi nào chúng tôi được yêu cầu tính toán C[n,k] cho một n và k cụ thể, chúng tôi bắt đầu bằng cách kiểm tra bảng để xem liệu đã được tính chưa. Nếu nó chưa được tính toán, chúng tôi tính toán nó và lưu giá trị vào bảng trước khi trả về kết quả

    subset[n - 1, k]
0

Thay thế đệ quy bằng một bảng

Một tác dụng phụ thú vị của giải pháp ghi nhớ là cuối cùng nó sẽ lấp đầy một bảng c[n][k] với các giá trị cho C[n,k]

Một cách tiếp cận trực tiếp hơn để thực hiện vấn đề kết hợp một cách hiệu quả là chỉ cần viết mã để lấp đầy bảng với các giá trị phù hợp

Chúng ta có thể làm điều này với một chức năng

    subset[n - 1, k]
1

Khi mảng c được điền đầy đủ các giá trị, chúng ta có thể viết lại hàm để tính toán các kết hợp

    subset[n - 1, k]
2

Để viết hàm initTable, chúng ta chỉ cần sao chép logic trong giải pháp đệ quy ban đầu

if k == 0:
    return 1
if n == k:
    return 1
2

trong một tập hợp các vòng lặp. Có hai điều chúng ta cần làm. Đầu tiên là viết một cặp vòng lặp điền vào các mục c[n][k] cho tất cả n và k trong các trường hợp cơ bản. Các trường hợp cơ sở cho sự kết hợp là k = 1 và k = n

    subset[n - 1, k]
4

Cuối cùng, chúng tôi xây dựng một vòng lặp điền vào c[n][k] cho tất cả các mục còn lại. Chúng ta phải cẩn thận khi xây dựng vòng lặp để đảm bảo rằng công thức cho c[n][k] chỉ sử dụng các mục mà vòng lặp đã điền sẵn. Đây là cấu trúc vòng lặp chính xác để làm điều này

    subset[n - 1, k]
5

hội nhập Romberg

Trong một bài giảng trước, chúng ta đã sử dụng quy tắc hình thang để ước tính diện tích dưới một đường cong. Ý tưởng cơ bản là chia phạm vi tích hợp thành một số lượng lớn các khoảng con. Trên mỗi khoảng con này, chúng ta dựng một hình thang có diện tích xấp xỉ diện tích dưới đường cong trên khoảng con đó

Diện tích của một hình thang như vậy là h [f[xi] + f[xi+1]]/2. Cộng tất cả các diện tích hình thang này lại sẽ cho chúng ta ước tính diện tích hình thang cho N khoảng con

Quy tắc hình thang tạo thành điểm khởi đầu cho một phương pháp phức tạp hơn. Bước đầu tiên trong phương pháp này là một kỹ thuật được gọi là ngoại suy Richardson

Phép ngoại suy Richardson bắt đầu bằng việc xem xét số hạng sai số trong quy tắc hình thang. Khi chúng ta triển khai quy tắc hình thang để ước tính tích phân với N khoảng con và kích thước bước là h, quy tắc hình thang thường tạo ra kết quả chính xác với hệ số tỷ lệ với h2

Ký hiệu O[h2] tôi sử dụng ở đây có nghĩa là lỗi thuộc thứ tự h2. Một cách khác để viết sự kiện này là viết số hạng sai số dưới dạng tổ hợp các lũy thừa khác nhau của h

[Điều này hơi nằm ngoài phạm vi thảo luận ở đây, nhưng hóa ra số hạng sai số từ quy tắc hình thang chỉ chứa các lũy thừa chẵn của h. ]

Bước tiếp theo là triển khai phép ngoại suy Richardson, đây là một thủ thuật đại số kỳ lạ nhưng hiệu quả. Trong thủ thuật này, chúng ta viết ra mối quan hệ lỗi hai lần, một lần cho N hình thang như chúng ta đã thấy ở trên và lần thứ hai cho 2 N hình thang. Việc tăng số lượng hình thang lên 2 lần sẽ giảm kích thước bước từ h xuống h/2, vì vậy chúng tôi nhận được hai biểu thức này cho lỗi

Nhân phương trình thứ hai với 4 và trừ đi phương trình đầu tiên từ nó sẽ tạo ra

Chúng ta có thể viết lại điều này như

hoặc

Bằng cách sử dụng thủ thuật đại số đơn giản này, chúng ta đã chuyển từ một phương pháp có số hạng sai số với độ lớn O[h2] sang một phương pháp có số hạng sai số với độ lớn O[h4]

Tốt hơn nữa, thủ thuật này có thể được lặp đi lặp lại. Tôi sẽ cung cấp cho bạn thông tin chi tiết, nhưng việc lặp lại thủ thuật này là cơ sở cho một kỹ thuật tích hợp có tên là tích hợp Romberg. Dưới đây là tóm tắt về kỹ thuật Romberg. Hàm R[k,j] đại diện cho lần lặp thứ j của kỹ thuật này bắt đầu từ quy tắc hình thang với N = 2k-1 bước

R[a,b,k,1] = Diện tích hình thang[a,b,2k-1]

Đây là một chương trình Python sử dụng công thức đệ quy này để tính các ước lượng tích phân

    subset[n - 1, k]
6

Phải mất một chút nỗ lực để giải nén những gì chương trình này đang làm. Vòng lặp ở phía dưới đang tính toán R[a,b,4 j,j] cho j trong phạm vi từ 2 đến 5. Đối với một giá trị nhất định của j, phương pháp này bắt đầu bằng ước tính quy tắc hình thang sử dụng 2k-1 = 24 j - 1 bước. Khi j = 5, điều này có nghĩa là sử dụng 220 - 1 = 524288 bước

Chương trình này có một lỗ hổng cuối cùng, và lỗ hổng này làm chậm quá trình tính toán bắt đầu từ khoảng j= 4. Vấn đề ở đây là một trong những đệ quy quá mức. Như bạn có thể thấy từ đoạn mã trên, một lệnh gọi thông thường tới hàm romberg sẽ kích hoạt ba lệnh gọi đệ quy bổ sung. Nếu mỗi trong số đó lần lượt kích hoạt nhiều cuộc gọi hơn đến chức năng romberg, thì cuối cùng bạn có thể thực hiện một số lượng lớn các cuộc gọi cho chức năng này

Để khắc phục vấn đề này, chúng ta có thể sử dụng một chiến lược bộ nhớ đệm đơn giản. Mỗi lần chúng ta tính toán một giá trị cho

if k == 0:
    return 1
if n == k:
    return 1
60 cho một tổ hợp cụ thể của k và j, chúng ta nên ghi lại kết quả đó. Lần tới khi chúng ta cần giá trị đó, chúng ta có thể chỉ cần tra cứu kết quả mà không cần phải tính toán lại bất cứ điều gì

Phiên bản cuối cùng của chương trình sử dụng chiến lược bộ nhớ đệm này. Chúng tôi thiết lập một danh sách hai chiều

if k == 0:
    return 1
if n == k:
    return 1
61có thể lưu trữ các giá trị cho nhiều tổ hợp k và j. Hàm
if k == 0:
    return 1
if n == k:
    return 1
62 sau đó giao hầu hết công việc cho một hàm bên trong đệ quy,
if k == 0:
    return 1
if n == k:
    return 1
63.
if k == 0:
    return 1
if n == k:
    return 1
63 thực hiện phép tính được tối ưu hóa bằng cách kiểm tra bộ đệm trước để xem kết quả đã được ghi cho tổ hợp k và j này chưa. Nếu vậy, nó sẽ dừng ngay lập tức và trả về giá trị đã lưu trong bộ nhớ cache. Nếu không, nó quay lại công thức đệ quy để tính toán kết quả, sau đó lưu trữ kết quả đó trước khi trả về câu trả lời

    subset[n - 1, k]
7

Lưu ý rằng vì hiện tại chúng ta có một phương pháp hiệu quả hơn nên chúng ta có thể mở rộng phạm vi của j mà chúng ta tính toán một cách an toàn. Đây là kết quả mà chương trình này trả về

    subset[n - 1, k]
8

Bài tập lập trình

Trong một trong những bài giảng đầu tiên của khóa học này, chúng ta đã thấy một ví dụ về bài toán nội suy đa thức. Cho một danh sách [x0 , y0] , [x1 , y1] , … , [xn , yn] các điểm để nội suy, chúng ta muốn xây dựng một đa thức bậc n đi qua các điểm

Newton đã giải bài toán này bằng cách tìm một đa thức có dạng đặc biệt, được gọi là đa thức Newton

Pn[x] = a0 + a1[x - x0] + a2[x - x0][x - x1] + ⋯ + an[x - x0][x - x1]⋯[x - xn-1]

Newton đã có thể đưa ra một mối quan hệ đệ quy mà các hệ số của đa thức này thỏa mãn. Ông giới thiệu một ký hiệu đặc biệt

Sử dụng ký hiệu này, các hệ số của đa thức Newton là

ak = f[0,k]

Viết chương trình có thể đọc danh sách các điểm dữ liệu từ tệp văn bản và xây dựng đa thức Newton nội suy các điểm. Mỗi dòng trong tệp văn bản chứa một giá trị x và một giá trị y. Chương trình của bạn nên đọc những điểm dữ liệu đó và lưu trữ chúng trong hai danh sách,

if k == 0:
    return 1
if n == k:
    return 1
65 và
if k == 0:
    return 1
if n == k:
    return 1
66. Sau đó, chương trình của bạn sẽ nhắc người dùng nhập giá trị cho x, sau đó tính toán và in ra giá trị của đa thức Newton tại x đó

Chương trình của bạn phải chứa ít nhất một hàm,

if k == 0:
    return 1
if n == k:
    return 1
67, để tính các thừa số f[i,j]. Bạn cũng có thể muốn viết hàm thứ hai,
if k == 0:
    return 1
if n == k:
    return 1
68 để tính số hạng thứ k của đa thức nội suy

f[0,k] [x- x0][x - x1]⋯[x - xk-1]

Để kiểm tra chương trình của bạn, đây là một số dữ liệu thử nghiệm

xy1. 00. 76519771. 30. 62008601. 60. 45540221. 90. 28181862. 20. 1103623

Đa thức bậc 4 đi qua các điểm này có giá trị xấp xỉ 0. 5118200 tại x = 1. 5. 

Chủ Đề