Để có thể đi sâu và nắm vững một cách có hệ thống kiến thức đã thu nhận được trong quá trình học môn Cấu trúc dữ liệu và giải thuật, em chọn đề tài “Với số tự nhiên n cho trước tính xem
Trang 1LỜI NÓI ĐẦU
Trong khoa học máy tính, cấu trúc dữ liệu là một cách lưu dữ liệu trong máy tính sao
cho nó có thể được sử dụng một cách hiệu quả Thông thường, một cấu trúc dữ liệu được chọn cẩn thận sẽ cho phép thực hiện thuật toán hiệu quả hơn Việc chọn cấu trúc dữ liệu thường bắt đầu
từ chọn một cấu trúc dữ liệu trừu tượng Một cấu trúc dữ liệu được thiết kế tốt cho phép thực hiện nhiều phép toán, sử dụng càng ít tài nguyên, thời gian xử lý và không gian bộ nhớ càng tốt Các cấu trúc dữ liệu được triển khai bằng cách sử dụng các kiểu dữ liệu, các tham chiếu và các phép toán trên đó được cung cấp bởi một ngôn ngữ lập trinh
Mỗi loại cấu trúc dữ liệu phù hợp với một vài loại ứng dụng khác nhau, một số cấu trúc dữ liệu dành cho những công việc đặc biệt
Trong thiết kế nhiều loại chương trình, việc chọn cấu trúc dữ liệu là vấn đề quan trọng Kinh nghiệm trong việc xây dựng các hệ thống lớn cho thấy khó khăn của việc triển khai chương trình, chất lượng và hiệu năng của kết quả cuối cùng phụ thuộc rất nhiều vào việc chọn cấu trúc
dữ liệu tốt nhất
Để có thể đi sâu và nắm vững một cách có hệ thống kiến thức đã thu nhận được trong quá
trình học môn Cấu trúc dữ liệu và giải thuật, em chọn đề tài “Với số tự nhiên n cho trước tính
xem có bao nhiêu cách biểu diễn n thành tổng của 1 hay nhiều số tự nhiên khác” để tìm hiểu
và nghiên cứu
Trong quá trình thực hiện đồ án, chúng em xin chân thành cảm ơn sự hướng dẫn tận tình của thầy Phan Thanh Tao đã giúp đỡ em hoàn thành tốt đồ án môn học này
Trang 2A Giới thiệu đề tài:
Tên đề tài: Với số tự nhiên n cho trước tính xem có bao nhiêu cách biểu diễn n thành tổng của 1 hay nhiều số tự nhiên khác(không tính đến thứ tự của các số hạng, ví dụ 3=2+1=1+2 coi như là một cách biểu diễn)
Có nhiều phương pháp đề giải quyết bài toán Trong đồ án này, chúng em phân tích và giải quyết bài toán theo 3 phương pháp để tìm lời giải tối ưu nhất:
Phương pháp quay lui( back tracking)
Phương pháp đệ quy(chia để trị)
Phương pháp quy hoạch động(Dynamic Programming)
Ứng với mỗi phương pháp gồm :
Có cơ sở lý thuyết
Phân tích hướng giải quyết
Cài đặt chương trình
Tài liệu tham khảo:
Giải thuật và lập trình – Lê Minh Hoàng
Thiết kế và đánh giá thuật toán – Trần Tuấn Minh
Kiến thức được học ở trường – Phan Thanh Tao
Trang 3B LÝ THUYẾT
I Nghiên cứu lý thuyết liên quan :
1.Thuật toán quay lui :
a Ý tưởng :
Nét đặt trưng của phương pháp quay lui là các bước hướng tới lời giải cuối cùng của mỗi bài toán hoàn thành được làm thử
Tại mỗi bước nếu có một lựa chọn được chấp nhận thì ghi nhận lựa chọn này và tiến hành các bước tiếp theo Còn ngược lại không có lựa chọn nào thích hợp thì làm lại bước trước, xóa bỏ ghi nhận này và quay về chu trình thử các lựa chọn còn lại
Hành động này được gọi là quay lui, thuật toán thể hiện phương pháp này được gọi là quay lui
Điểm quan trọng của thuật toán là phải ghi nhớ tại mỗi bước đi qua để tránh trùng lặp khi quay lui dễ thấy là thông tin này cần lưu trữ vào một ngăn xếp nên thuật toán thể hiện ý thiết kế một cách đệ quy
b Mô hình :
Lời giải của bài toán thường biểu diễn bằng một vecto gồm n thành phần x=(x1…xn) phải thỏa mãn các điều kiện nào đó Để chỉ ra lời giải x ta phải xây dựng dần các thành phần xi
Tại mỗi bước i :
Đã xây dựng xong các thành phần x1 xi-1
Xây dựng thành phần xi bằng cách lần lượt thử tất cả các khả năng mà xi có thể chọn
Nếu 1 khả năng j nào đó phù hợp cho xi thì xác định xi theo khả năng j Thường phải có thêm thao tác ghi nhận trạng thái mới của bài toán để hổ trợ
Trang 4cho bước quay lui Nếu i=n thì được một lời giải ngược lại thì tiến hành bước i+1 để xác định xi+1
Nếu không có khả năng nào chấp nhận cho xi thì ta lùi lại bước trước i-1 để xác định thành phần xi-1
Để đơn giản, ta giả định các khả năng lựa chọn cho các xi tại mỗi bước là như nhau, do đó phải có thêm một thao tác kiểm tra khả năng j nào là chấp nhận được cho xi
Mô hình của phương pháp quay lui có thể viết bằng thủ tục sau với n là số bước cần phải thực hiện, k là số khả năng mà xi có thể lựa chọn
Try(i)
{ for(j=1 → k)
if(xi chấp nhận được khả năng j) { Xác định xi theo khả năng j;
Ghi nhận trạng thái mới;
If(i<n) Try(i+1) Else Ghi nhận nghiệm;
Trả lại trạng thái cũ;
} }
Trang 52.Đệ quy và giải thuật đệ quy :
a Khái niệm :
Ta nói một đối tượng là đệ quy nếu nó được định nghĩa qua chính nó hoặc một đối tượng khác cùng dạng với chính nó bằng quy nạp
b Giải thuật :
Nếu lời giải của một bài toán P được thực hiện bằng lời giải của bài toán P’ có dạng giống như P thì đó là một lời giải đệ quy Giải thuật tương ứng với lời giải như vậy gợi là giải thuật đệ quy
Lưu ý : P’ tuy có dạng giống như P, nhưng theo một nghĩa nào đó, P’ phải «nhỏ » hơn P ,
dễ giải hơn P và việc giải nó không câng dùng đến P
Định nghĩa một hàm đệ quy hay một thủ tục đệ quy gồm hai phần :
Phần neo(anchor) : Phần này được thực hiện khi mà công việc quá đơn giản, có thể giải trực tiếp chứ không cần phải nhờ đến một bài toán con nào cả
Phần đệ quy : Trong trường hợp bài toán chưa thể giải được bằng phần neo, ta xác định nhữn bài toán con và gọi đệ quy giải những bài toán con đó Khi đã có lời giải (đáp số) của những bài toán con rồi thì phối hợp chúng lại để giải bài toán đang quan tâm
Phần đệ quy thể hiện tính « quy nạp » của lời giải Phần neo cũng rất quan trọng bởi nó quyết định tới tính hữu hạn dừng của lời giải
c Ví dụ về giải thuật đệ quy :
Hàm tính giai thừa :
Int factorial(int n)
Trang 6If (n==0) return 1 ; // phần neo
Else return n* factoria l(n-1) ; // phần đệ quy
}
Ở đây, phần neo định nghĩa kết quả hàm tịa n=0, còn phần đệ quy (ứng với n>0) sẽ định nghĩa kết quả hàm qua giá trị của n và giai thừa của n-1
Ta thử tính với 3 ! thì trước hết nó phải đi tính 2 ! bởi 3! được tính bằng tích của 3*2 ! Tương tự để tính 2 !, nó lại tính 1 ! bởi 2 !=2*1 ! Áp dung bước quy nạp này thêm một lần nữa , 1 !=1*0 ! , và đạt tới trường hợp của phần neo, đến giá trị 1 của 0 ! , nó tính được 1 != 1*1 ;
từ giá trị của 1 ! tính được 2 !; từ giá trị của 2 ! tính được 3 !; cuối cùng cho kết quá là 6 :
3.Quy hoạch động:
Đối với nhiều thuật toán, phương pháp chia để trị thường đóng vai trò chủ đạo trong việc thiết kế thuật toán trong phương pháp quy hoạch động lại càng tận dụng phương pháp này: khi không biết cần phải giải bài toán con nào, ta giải tất cả các bài toán con và lưu trữ những lời giải này(để khỏi phải tính toán lại ) nhằm sử dụng lại chúng để giải quyết bài toán lớn hơn
Phương pháp này tổ chức tìm kiếm lời giải theo kiểu từ dưới lên Xuất phát từ các bài toán nhỏ và đơn giản nhất, tổ hợp các lời giải của chúng để có lời giải của bài toán lớn hơn Và cứ như thế để tìm lời giải của bài toán ban đầu
Trang 7Khi sử dụng phương pháp này để giải quyết vấn đề ta có thể gặp 2 khó khăn sau:
1 Số lượng lời giải của bài toán con có thể rất lớn không thể chấp nhận được
2 Không phải lúc nào sự kết hợp lời giải của các bài toán con cũng cho ra lời giải của bài toán lớn
Để giải quyết những trường hợp như vậy phương pháp quy hoạch động dựa vào một nguyên lý gọi là nguyên lý tối ưu của Bellman:
“Nếu lời giải bài toán là tối ưu thì lời giải của các bài toán con cũng tối ưu”
Trong thuật toán quy hoạch động thường dùng các thao tác:
Xây dựng một hàm quy hoạch động(hoặc phương trình quy hoạch động)
Lập bản lưu lại các giá trị của hàm
Truy xuất lời giải tối ưu của bài toán từ bảng lưu
II Mô tả bài toán :
1.Yêu cầu :
Nhập từ bàn phím một số n và in ra màng hình số cách phân tích n thành tổng của dãy các
số nguyên dương, các cách phân tích là hoán vị của nhau thì chỉ tính là 1
2.Ví dụ :
N = 5 thì có 7 cách phân tích :
1 5= 1+1+1+1+1
2 5= 1+1+1+2
3 5= 1+1+3
4 5= 1+2+2
5 5= 1+4
6 5= 2+3
7 5= 5
Trang 8C GIẢI THUẬT
I Thuật toán quay lui :
1.Phương thức :
Giải thuật này sẽ liệt kê tất cả các cách phân tích 1 số n thành tổng của dãy các số nguyên dương
2.Giải thuật :
Ta sẽ lưu nghiệm trong mảng x, ngoài ra có một mảng t Mảng t xây dựng như sau : t[i] sẽ
là tổng các phần tử trong mảng x từ x[1] đến x[i] : t[i]= x[1] + x[2] + … + x[i]
Khi liệt kê các dãy x có tổng các phần tử đúng bằng n, để tránh trùng lặp ta đưa thêm ràng buộc
Vì số phần tử thực sự của mảng x là không cố định nên thủ tục in() dùng để in ra 1 cách phân tích phải có thêm tham số cho biết sẽ in ra bao nhiêu phần tử
Thủ tục đệ quy thu(i) sẽ thử các giá trị có thể nhận của x[i]( ) Để tổng quát cho i=1, đặt x[0]=1 và t[0]= 0
Xét các giá trị của x[i] từ x[i-1] đến (n-t[i-1]) div 2, cập nhật t[i]=t[i-1]+x[i]
và gọi đệ quy tiếp
Cuối cùng xét giá trị x[i]=n-t[i-1] và in kết quả từ x[1] đến x[i]
Input : nhập từ bàn phím sô nguyên dương n<100
Output : xuất ra các cách phân tích số n
Trang 93 Chương trình:
#include<conio.h>
#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<dos.h>
#define max 100
int x[max],t[max],n;long count;
void in(int k)
{int i;
printf("\ncach %ld:: %d=",count++,n);
for (i=1;i<k;i++)
printf("%d+",x[i]);
printf("%d",x[k]);
}
void thu(int i)
{int j;
for (j=x[i-1];j<=(n-t[i-1])/2;j++)
{x[i]=j;
t[i]=t[i-1]+j;
thu(i+1);
}
x[i]=n-t[i-1];
in(i);
}
Trang 10{
count=1;
do {printf("\nn=");scanf("%d",&n);} while (n>40);
x[0]=1;
t[0]=0;
thu(1);
getch();
}
II Thuật toán đệ quy :
1.Phương thức :
Với phương thức sử dụng ở thuật toán quay lui chúng ta sẽ gặp rắc rối ở những trường hợp n tương đối lớn vì khi đó in số cách phân tích là rất lớn Ở đây chúng ta chỉ in ra có bao nhiêu cách chứ không liệt kê ra các cách theo phương pháp đệ quy Để làm được điều đó ta cần xây dựng công thức truy hồi cần thiết
2 Giải thuật xây dựng công thức truy hồi:
Gọi F[m,v] là sô cách phân tích số v thành tổng các số nguyên dương m Khi đó :
Các cách phân tích số v thành tổng các số nguyên dương m có thể chia làm 2 loại :
Loại 1 : không chứa số m trong phép phân tích, khi đó số cách phân tích loại này chính là số cách phân tích số n thành tổng các số nguyên dương < m, tức là số cách phân tích số v thành tổng các số nguyên dương m-1 và bằng F[m-1,v]
Loại 2 : có chưa ít nhất 1 số m trong phép phân tích Khi đó nếu trong các cách phân tích loại này ta bỏ đi số m đó thì ta sẽ được các cách phân tích sô v – m thành tổng các số nguyên dương m(Lưu ý : điều này chỉ đúng khi không tính lặp lại các hoán
vị của 1 cách) Có nghĩa là về mặt số lượng, số các cách phân tích loại này bằng F[m, v-m]
Trang 11Trong trường hợp m>v thì rõ ràng chỉ có các cách phân tích loại 1, còn trong trường hợp m
v thì sẽ có cả các cách phân tích loại 1 và loại 2 Vì thế :
F[m-1, v] nếu m>v
F[m,v]=
F[m-1,v] +F[m, v-m] nếu m v
Ta có công thức xây dựng F[m,v] từ F[m-1,v] và F[m,v-m] Công thức này có tên gọi là công thức truy hồi đưa việc tính F[m,v] về việc tính F[m’,v’] với dữ liệu nhỏ hơn Tất nhiên, cuối cùng ta sẽ quan tâm đến F[n,n] : số các cách phân tích số n thành tổng các số nguyên dương n
3 Cài đặt đệ quy:
#include<conio.h>
#include<math.h>
#include<stdio.h>
long f(int m,int v)
{
if (m==0)
{
if (v==0) return 1;
else return 0;
}
else
{
if (m>v) return f(m-1,v);
else return (f(m-1,v)+f(m,v-m)) ;
}
}
main()
{
int m,n,v;
do {printf("n=");scanf("%d",&n);} while (n>80);
printf("\n so cach phan tich =%ld",f(n,n));
Trang 12getch();
}
III Quy hoạch động:
1 Phương thức:
Từ công thức truy hồi được xây dựng ở trên :
F[m-1, v] nếu m>v
F[m,v]=
F[m-1,v] +F[m, v-m] nếu m v
Ta có thể xây dựng bảng F để giải quyết bài toán theo phương pháp quy hoạch động
2 Giải thuật :
Khởi tạo dòng 0 của bảng, sau đó dùng dòng 0 tính dòng 1, dùng dòng 1 tính dòng 2 v.v… tới khi tính được hết dòng n Mỗi giá trị được tính theo công thức truy hồi trên và lưu vào mảng F[m,v] số cách phân tích là F[n,n]
Ví dụ : n=5 ta có bảng sau :
Trang 13Nhìn vào bảng F , ta thấy rằng F[m,v] được tính bằng tổng của :
Một phần tử ở hành trên :F[m-1,v] và 1 thành phần ở cùng hàng bên trái F[m,v-m]
Ví dụ : F[5,5] sẽ được tính bằng F[4, 5] + F[5, 0], hay F[3, 5] sẽ được tính bằng F[2, 5] + F[3, 2] Chính vì vậy để tính F[m, v] thì F[m - 1, v] và F[m, v - m] phải được tính trước Suy ra thứ tự hợp lý để tính các phần tử trong bảng F sẽ phải là theo thứ tự từ trên xuống và trên mỗi hàng thì tính theo thứ tự từ trái qua phải Điều đó có nghĩa là ban đầu ta phải tính hàng 0 của bảng: F[0, n] = số dãy có các phần tử ≤ 0 mà tổng bằng n, theo quy ước ở đề bài thì F[0, 0] = 1 còn F[0, n] với mọi n > 0 đều là 0
Vậy giải thuật dựng rất đơn giản: Khởi tạo dòng 0 của bảng F: F[0, 0] = 1 còn F[0, v] với mọi v > 0 đều bằng 0, sau đó dùng công thức truy hồi tính ra tất cả các phần tử của bảng F Cuối cùng F[n, n] là số cách phân tích cần tìm
3.Chương trình :
a Dạng 1 :
#include<stdio.h>
#include<conio.h>
#define max 100
{
int F[max][max];
int n, m, v;
scanf("%d",&n);
for(v=0;v<=n;v++) F[0][v]=0;
F[0][0]=1;
for(m=1;m<=n;m++)
for(v=0;v<=n;v++)
{
if(v<m) F[m][v]=F[m-1][v];
else F[m][v]=F[m-1][v]+F[m][v-m];
}
Trang 14b Dạng 2 (cải tiến) :
Ta nhận thấy tại mỗi bước, ta chỉ cần lưu lại một dòng của bảng F bằng một mảng 1 chiều, sau đó dùng mảng đó tính lại chính nó để sau khi tính, mảng một chiều sẽ lưu các giá trị của bảng
F trên dòng kế tiếp Không cần dùng mảng 2 chiều để tối ưu và tiết kiệm không gian nhớ
#include<stdio.h>
#include<conio.h>
int main()
{
unsigned long f[1000];
int v,m,n,i;
printf("Nhap so nguyen duong n= ");
scanf("%ld",&n);
while (n<0)
{
printf("Nhap so nguyen duong n= ");
scanf("%ld",&n);
}
for (i=0;i<=n;i++) f[i]=0;
f[0]=1;
for (m=1;m<=n;m++)
for (v=m;v<=n;v++)
f[v]=f[v]+f[v-m];
printf("Co %ld cach phan tich.\n",f[n]);
getch();
return 0;
}
Trang 15D Kết quả chạy chương trình
Các thuật toán được mô tả trong chương trình tổng hợp được thực hiện bằng ngôn ngữ lập trình Java
1 Giới thiệu
Trang 162.Quay lui:
3 Quy hoạch động:
Trang 174 Đệ quy:
Trang 18E Kết Luận
Với thuật toán quay lui, ngoài việc cho biết số cách phân tích được mà còn liệt kê được tất
cả các cấu hình cần tìm giúp ta dễ kiểm tra Tuy nhiên, phương pháp này chỉ thực sự hữu dụng đối với n nhỏ, trường hợp n lớn ta sẽ gặp rắc rối bởi số cấu hình cần in là rất lớn Phương pháp này có độ phức tạp hàm mũ (O (nn))
Với thuật toán đệ quy (chia để trị), quá trình cài đặt đơn giản, tuy nhiên chỉ cho biết số cách phân tích, với việc sử dụng ngăn xếp để lưu giá trị nên tốn bộ nhớ Phương pháp này cũng có
độ phức tạp hàm mũ (O (2n))
Với thuật toán quy hoạch động, ta tiết kiệm được bộ nhớ, có thể thực hiện với n lớn Tuy nhiên, nó cũng chỉ cho ta biết được số cách phân tích Phương pháp này có độ phức tạp O(n2) Phương pháp này tối ưu nhất (trong trường hợp bài toán ta đang xét)