Thứ Bảy, 17 tháng 9, 2011

CTDL: Cây Nhị Phân Tìm Kiếm


1. CÂY NHỊ PHÂN TÌM KIẾM
Cây nhị phân tìm kiếm (CNPTK) là cây nhị phân trong đó tại mỗi nút, khóa của nút đang xét lớn hơn khóa của tất cả các nút thuộc cây con trái và nhỏ hơn khóa của tất cả các nút thuộc cây con phải. Dưới đây là một ví dụ về cây nhị phân tìm kiếm:
  
Nhờ ràng buộc về khóa trên CNPTK, việc tìm kiếm trở nên có định hướng. Hơn nữa, do cấu trúc cây việc tìm kiếm trở nên nhanh đáng kể. Nếu số nút trên cây là N thì chi phí tìm kiếm trung bình chỉ khoảng log2N. 
Trong thực tế, khi xét đến cây nhị phân chủ yếu người ta xét CNPTK.2. CÁC THAO TÁC TRÊN CÂY NHỊ PHÂN TÌM KIẾM
2.1. Duyệt cây
Thao tác duyệt cây trên cây nhị phân tìm kiếm hoàn toàn giống như trên cây nhị phân. Chỉ có một lưu ý nhỏ là khi duyệt theo thứ tự giữa, trình tự các nút duyệt qua sẽ cho ta một dãy các nút theo thứ tự tăng dần của khóa.  
2.2. Tìm một phần tử x trong cây
LPNODE searchNode(TREE T, Data X)
{
      if ( T != NULL )    
      {
            if(T->Key == X)   
                  return T;
            else if(T->Key > X)
                  return searchNode(T->pLeft, X);
            else
                  return searchNode(T->pRight, X);
      }
 
      return NULL;
}
 
//Ta có thể xây dựng một hàm tìm kiếm tương đương không đệ qui như sau: 
LPTNODE searchNode(TREE Root,  Data x)
{    
      LPNODE p = Root;
      while (p != NULL) 
      {
            if(x == p->Key) 
                  return p;
            else if(x < p->Key) 
                  p = p->pLeft;
            else //if(x > p->Key) 
                  p = p->pRight;
      }
      return NULL;
}
Dễ dàng thấy rằng số lần so sánh tối đa phải thực hiện để tìm phần tử X là h, với h là chiều cao của cây. Như vậy thao tác tìm kiếm trên CNPTK có n nút tốn chi phí trung bình khoảng O(log2n) .  
Ví dụ: Tìm phần tử 55
2.3. Thêm một phần tử x vào cây Việc thêm một phần tử X vào cây phải bảo đảm điều kiện ràng buộc của CNPTK. Ta có thể thêm vào nhiều chỗ khác nhau trên cây, nhưng nếu thêm vào một nút lá sẽ là tiện lợi nhất do ta có thể thực hiên quá trình tương tự thao tác tìm kiếm. Khi chấm dứt quá trình tìm kiếm cũng chính là lúc tìm được chỗ cần thêm. 
Hàm insert trả về giá trị –1, 0, 1 khi không đủ bộ nhớ, gặp nút cũ hay thành công: 
int insertNode(TREE &T, Data X)
{
      if ( T != NULL )    
      {
            if (T->Key == X)   
                  return 0; //đã có
            else if (T->Key > X)
                  return insertNode(T->pLeft, X);
            else
                  return insertNode(T->pRight, X);
      }
 
      T  = new TNode;
      if (T == NULL)  
            return -1; //thiếu bộ nhớ  
      T->Key   = X;
      T->pLeft = NULL;
      T->pRight = NULL;
 
      return 1;  //thêm vào thành công
}
Ví dụ: Thêm phần tử 50
    
2.4. Hủy một phần tử có khóa X
Việc hủy một phần tử X ra khỏi cây phải bảo đảm điều kiện ràng buộc của CNPTK. 
Có 3 trường hợp khi hủy nút X có thể xảy ra:
X là nút lá.
X chỉ có 1 con (trái hoặc phải).
X có đủ cả 2 con
Trường hợp thứ nhất: chỉ đơn giản hủy X vì nó không móc nối đến phần tử nào khác.
 
Trường hợp thứ hai: trước khi hủy X ta móc nối cha của X với con duy nhất của nó.  
 
Trường hợp cuối cùng: ta không thể hủy trực tiếp do X có đủ 2 con Þ Ta sẽ hủy gián tiếp. Thay vì hủy X, ta sẽ tìm một phần tử thế mạng Y. Phần tử này có tối đa một con. Thông tin lưu tại Y sẽ được chuyển lên lưu tại X. Sau đó, nút bị hủy thật sự sẽ là Y giống như 2 trường hợp đầu. 
Vấn đề là phải chọn Y sao cho khi lưu Y vào vị trí của X, cây vẫn là CNPTK. 

Có 2 phần tử thỏa mãn yêu cầu: 
+ Phần tử nhỏ nhất (trái nhất) trên cây con phải.
+ Phần tử lớn nhất (phải nhất) trên cây con trái.

Việc chọn lựa phần tử nào là phần tử thế mạng hoàn toàn phụ thuộc vào ý thích của người lập trình. Ở đây, cháng tôi sẽ chọn phần tử (phải nhất trên cây con trái làm phân tử thế mạng.
Hãy xem ví dụ dưới đây để hiểu rõ hơn:
Sau khi hủy phần tử X=18 ra khỏi cây tình trạng của cây sẽ như trong hình (phần tử 23 là phần tử thế mạng)Hàm deleteNode trả về giá trị 1, 0 khi hủy thành công hoặc không có X trong cây:
int deleteNode(TREE &T, Data X)
{
      if (T == NULL)    
            return 0;
      else if (T->Key > X)
            return deleteNode (T->pLeft, X);
      else if(T->Key < X)
            return deleteNode (T->pRight, X);
      else //T->Key == X
      { 
            LPNODE p = T;
            if (T->pLeft == NULL)
                  T = T->pRight;
            else if (T->pRight == NULL)
                  T = T->pLeft;
            else //T có cả 2 con 
            {             
                  LPNODE q = T->pRight;
                  searchStandFor(p, q);
            }
            delete p;
      }
}
Trong đó, hàm searchStandFor được viết như sau: 
//Tìm phần tử thế mạng cho nút p 
void searchStandFor(TREE &p, TREE &q)
{
      if (q->pLeft)
            searchStandFor(p, q->pLeft);
      else  
      {
            p->Key = q->Key;
            p      = q;
            q      = q->pRight;
      }
}2.5. Tạo một cây CNPTK
Ta có thể tạo một cây nhị phân tìm kiếm bằng cách lặp lại quá trình thêm 1 phần tử vào một cây rỗng.
2.6.  Hủy toàn bộ CNPTK
Việc toàn bộ cây có thể được thực hiện thông qua thao tác duyệt cây theo thứ tự sau. Nghĩa là ta sẽ hủy cây con trái, cây con phải rồi mới hủy nút gốc. 
void removeTree(TREE &T)
{
      if ( T != NULL )
      {
            removeTree(T->pLeft);
            removeTree(T->pRight);
            delete T;
      }
}
3. ĐÁNH GIÁ
Tất cả các thao tác searchNode, insertNode, deleteNode trên CNPTK đều có độ phức tạp trung bình O(h), với h là chiều cao của cây. Trong trong trường hợp tốt nhất, CNPTK có n nút sẽ có độ cao h = log2(n). Chi phí tìm kiếm khi đó sẽ tương đương tìm kiếm nhị phân trên mảng có thứ tự. Tuy nhiên, trong trường hợp xấu nhất, cây có thể bị suy biến thành 1 DSLK (khi mà mỗi nút đều chỉ có 1 con trừ nút lá). Lúc đó các thao tác trên sẽ có độ phức tạp O(n). Vì vậy cần có cải tiến cấu trúc của CNPTK để đạt được chi phí cho các thao tác là log2(n).
Read More...

Thao tác với các số nguyên lớn trong C

Như chúng ta biết, số nguyên bình thường sẽ chiếm 2 byte bộ nhớ (kiểu int); lớn hơn nữa là kiểu long chiếm 4 byte, và cao hơn là kiểu long double với kích thước 10 byte, với việc thao tác trên các kiểu dữ liệu thông thường này thì việc thực hiện hoàn toàn dễ dàng (miễn sao giá trị trả về cũng nằm trong kích thước dữ liệu quy định, nếu không sẽ hiển thị không đúng); song song với các nhiệm vụ thông thường trên thì ngày nay chúng ta bắt gặp những vấn đề phải mở rộng các số nguyên mà chúng ta gọi là Số nguyên lớn! Máy tính sẽ chẳng hiểu số nguyên lớn là gì cả; hơn thế số nguyên lớn cũng chỉ mang tính “định tính” và mơ hồ cho những người còn mập mờ giữa các kiểu số nguyên thông thường! Vậy số nguyên lớn là gì? Số nguyên lớn theo ý chủ quan của tôi là Số nguyên toán học với kích thước lớn hơn kích thước Max lưu trữ thông thường của máy tính; như vậy với ngôn ngữ C thì Số nguyên lớn có kích thước lớn hơn 10 byte! (Tôi hiểu vậy; có anh em nào có ý kiến khác thì chúng ta mở rộng bàn thêm). Vấn đề lưu trữ số nguyên lớn trong máy thông thường sẽ được chúng ta quy về dạng dữ liệu string (mảng 1chiều). Tham gia Congdongcviet.com tôi đã nhận được khá nhiều các Members hỏi về vấn đề này (trên diễn đàn cũng hỏi, và qua hộp thư riêng cũng hỏi), trước yêu cầu đó tôi xin mạnh dạn nêu lên chủ đề này (như lời hứa) để hướng dẫn các Members trên diễn đàn, đồng thời cũng là đề tài mở cho anh em thảo luận bàn thêm (xây dựng tính hoàn chỉnh chủ đề); còn với bản thân cũng đưa ra một tư tưởng sau khi nghiên cứu vấn đề này từ các vị tiền bối đi trước (cũng đã có rất nhiều công trình đã được công bố); vậy mong anh em ủng hộ chủ đề. Peter chân thành cảm ơn!


Bố cục của chủ đề: Chủ đề sẽ hướng dẫn các bạn qua 4 phần:
Code:
- Phần 1: Thao tác toán Cộng
- Phần 2: Thao tác toán Trừ
- Phần 3: Thao tác toán Nhân
- Phần 4: Thao tác toán Chia
Phần 1: Thao tác toán Cộng

1. Thuật toán
Giả sử chúng ta có một số nguyên (lớn) a=32145; sẽ có rất nhiều cách lưu trữ số nguyên này (do ta quy định), ở đây Peter sẽ đưa ra cách lưu số này qua mảng 1 chiều ngược, cụ thể như sau: a[0]=5, a[1]=4, a[2]=1, a[3]=2 và a[4]=3; nhưng khi xuất thì lại xuất ngược lại so với cách lưu trữ!, số nguyên này có 5 chữ số, khi nhập (input) số này vào chương trình thì chúng ta sẽ dùng dấu khoảng trắng (space) để ngăn cách các chữ số và nhập bình thường theo thứ tự thông thường, ví dụ a=32145 sẽ được nhập vào như sau: 3 2 1 4 5.
Vậy nếu cần thực hiện phép toán Cộng hai số a và b sau đó lưu vào mảng c (kết quả) thì Peter đề xuất các bước (c=a+b):
- Bước 1: Loại bỏ các chữ số 0 vô nghĩa ở 2 mảng a và b.
- Bước 2: Thêm chữ số 0 ở đầu mảng có độ dài ngắn hơn để 2 mảng có cùng độ dài (tức là nếu a=1234, b=21 thì chúng ta thấy: mảng b có 2 phần tử; mảng a có 4 phần tử; vậy để hai mảng này có số phần tử bằng nhau thì chúng ta thêm 2 chữ số 0 nữa vào mảng b; lúc này b=0021).
- Bước 3: Dùng một biến nhớ, ký hiệu là nho, để lưu trữ số nhớ sau mỗi bước tính. Được khởi tạo có giá trị là 0. Trong một bước tính thì ở vị trí thứ i, các số được tính như sau:

Code:
c[i] = (a[i]+b[i]+nho)%10;
nho = (a[i]+b[i]+nho)/10.
Và cuối cùng, nếu vẫn còn nhớ thì chúng ta thêm một phần tử nữa vào mảng c mang giá trị bằng nho.
- Bước 4: Xuất kết quả theo thứ tự ngược của mảng c; đây là kết quả của phép tính.

Để làm các bạn hiểu tôi xin trình bày các bước trên với việc cộng hai số nguyên sau (để tiện không phải viết nhiều tôi sẽ thao tác với 2 số nguyên thông thường....). Thực hiện với 2 số: a=349 và b=35; c=a+b:
- Bước 1: Loại bỏ các chữ số 0 vô nghĩa; cả 2 số này đều không cần bước này.
- Bước 2: Thêm số 0 vào trước mảng b; lúc này b=035.
+ Cách lưu trữ của a là: 9 4 3 (tương ứng a[0]=9, a[1]=4 và a[2]=3).
+ Cách lưu trữ của b là: 5 3 0 (tương ứng b[0]=5, b[1]=3 và b[2]=0).
- Bước 3: Khởi tạo nho=0 và tính c[i] và nho sau mỗi bước nhỏ:
+ c[0] = (a[0]+b[0]+nho)%10 = (9+5+0)%10 = 4;
nho = (a[0]+b[0]+nho)/10 = (9+5+0)/10 = 1.
+ c[1] = (a[1]+b[1]+nho)%10 = (4+3+1)%10 = 8;
nho = (a[1]+b[1]+nho)/10 = (4+3+1)/10 = 0.
+ c[2] = (a[2]+b[2]+nho)%10 = (3+0+0)%10 = 3;
nho = (a[2]+b[2]+nho)/10 = (3+0+0)/10 = 0.
Vậy mảng c được lưu trữ là: 4 8 3 0.
- Bước 4: Xuất kết quả, sắp xếp ngược lại ta có c=0384 (chính là 384).

2. Tổ chức chương trình theo giải thuật
Sau khi phân tích và đưa ra giải thuật chúng ta đi vào việc xây dựng chương trình thực hiện; trong chương trình Peter mạnh dạn đưa biến global (các bạn có thể tuỳ mà làm, thế nào cũng được). Với lưu ý như sau:
- Nhập vào số các chữ số của số trước khi nhập lần lượt các chữ số.
- Nhập vào các chữ số ngăn cách nhau bằng dấu khoảng trắng (space) như đã nói phía trên.
Các bạn tự tìm hiểu chương trình này.

Code:
#include <stdio.h>
#include <conio.h>
int m,n,dem,nho=0,s,i,j,a[100],b[100],c[100]; //Gia su so nguyen lon co 100 chu so; tuy y!
void nhap()
{
    printf("Nhap so chu so cua so nguyen a, m= ");
    scanf("%d",&m);
    printf("Nhap lan luot cac chu so cua a (ngan cach nhau bang space):\n");
    for(i=m-1;i>=0;i--)
        scanf("%d",&a[i]);
    printf("\n\nNhap so chu so cua so nguyen b, n= ");
    scanf("%d",&n);
    printf("Nhap lan luot cac chu so cua b (ngan cach nhau bang space):\n");
    for(i=n-1;i>=0;i--)
        scanf("%d",&b[i]);
    //Buoc 1: Loai bo cac chu so 0 vo nghia o mang a va b
    while((a[m-1]==0)&&(m>0))
        m--;
    if(m==0)
        a[m++]=0;
    while((b[n-1]==0)&&(n>0))
        n--;
    if(n==0)
        b[n++]=0;
    //Buoc 2: Them cac chu so 0 vao dau cua mang co so phan tu be hon
    if(m<n)
        for(i=1;i<=n-m;i++)
            a[m-1+i]=0;
    else 
        for(i=1;i<=m-n;i++)
            b[n-1+i]=0;
    if(m<n)m=n;
    else 
        n=m;
}
void cong(int *a,int *b) //Buoc 3: Tinh c[i] va nho
{
    for(i=0;i<m;i++)
    {
        c[i]=(a[i]+b[i]+nho)%10;
        nho=(a[i]+b[i]+nho)/10;
    }
    if(nho>0)
        c[m++]=nho; 
}
int main()
{
    nhap();
    cong(a,b);
    printf("\n\nTong la: \n");    
    for(i=m-1;i>=0;i--)    //Buoc 4: Xuat ket qua
        printf("%d",c[i]);
    getch();
    return 0;
}
(tác giả:
peterdrew
nguồn: congdongCviet)

---------- Bài thêm lúc 14:29 ---------- Bài trước là lúc 14:21 ----------
Phần 2: Thao tác phép toán Trừ
1. Thuật toán
Lưu trữ các số hạng của phép trừ cũng thực hiện tương tự như thực hiện với phép toán cộng, khi xuất thì cũng xuất theo thứ tự ngược lại; các chữ số cũng được ngăn cách nhau bằng một khoảng trắng (space).
Việc tiếp là kiểm tra xem số trừ lớn hơn hay nhỏ hơn số bị trừ (?!), để thực hiện điều này chúng ta phải xây dựng một thủ tục kiểm tra, nếu lớn hơn thì thực hiện thêm một thao tác nữa là đảo lại hai mảng này và thực hiện bình thường theo thuật toán sau và sau đó thêm dấu “-“ vào kết quả: Giả sử cần thực hiện phép toán Trừ hai số a và b sau đó lưu vào mảng c (kết quả) thì Peter đề xuất các bước (c=a-b):
- Bước 1: Loại bỏ các chữ số 0 vô nghĩa ở 2 mảng a và b.
- Bước 2: Thêm chữ số 0 ở đầu mảng có độ dài ngắn hơn để 2 mảng có cùng độ dài (tức là nếu a=1234, b=21 thì chúng ta thấy: mảng b có 2 phần tử; mảng a có 4 phần tử; vậy để hai mảng này có số phần tử bằng nhau thì chúng ta thêm 2 chữ số 0 nữa vào mảng b; lúc này b=0021).
- Bước 3: Dùng một biến nhớ, ký hiệu là nho, để lưu trữ số nhớ sau mỗi bước tính. Được khởi tạo có giá trị là 0. Trong một bước tính thì ở vị trí thứ i, các số được tính như sau:
+ Nếu a[i]-nho>=b[i] thì

Code:
c[i]=(a[i]-b[i]-nho)%10;
nho=0.
+ Ngược lại:

Code:
c[i]=(a[i]+10-nho-b[i])%10;
nho=1.
- Bước 4: Giảm chiều dài mảng c (kết quả) khi có phần tử 0 đứng đầu; xuất kết quả!
Sau đây Peter sẽ ví dụ cho các bạn hiểu về thuật toán trên: Ta cần thực hiện a-b=c với a=349 và b=35.
- Bước 1: Loại bỏ các chữ số 0 vô nghĩa; cả 2 số này đều không cần bước này.
- Bước 2: Thêm số 0 vào trước mảng b; lúc này b=035.
+ Cách lưu trữ của a là: 9 4 3 (tương ứng a[0]=9, a[1]=4 và a[2]=3).
+ Cách lưu trữ của b là: 5 3 0 (tương ứng b[0]=5, b[1]=3 và b[2]=0).
- Bước 3: Khởi tạo nho=0 và tính c[i] và nho:
+ Kiểm tra a[0]-nho và b[0]:
a[0]-nho=9-0=9; b[0]=5 nên a[0]-nho>b[0], vậy:
c[0] = (a[0]-b[0]-nho)%10 = (9-5-0)%10 = 4;
nho = 0.
+ Kiểm tra a[1]-nho và b[1]:
a[1]-nho=4-0=4; b[1]=3 nên a[1]-nho>b[1], vậy:
c[1] = (a[1]-b[1]-nho)%10 = (4-3-0)%10 = 1;
nho = 0.
+ Kiểm tra a[2]-nho và b[2]:
a[2]-nho=3-0=3; b[2]=0 nên a[2]-nho>b[2], vậy:
c[2] = (a[2]-b[2]-nho)%10 = (3-0-0)%10 = 3;
nho = 0.
Vậy mảng c được lưu trữ là: 4 1 3.
- Bước 4: Xuất kết quả, sắp xếp ngược lại ta có c=314.
Rất lấy làm không tự nhiên khi lấy một ví dụ không có tính tổng quát, tuy nhiên nó cũng là ví dụ để minh chứng cho thuật toán thôi! Peter mong các bạn tiếp tục lấy các ví dụ khác để hiểu kỹ hơn về thuật toán này, còn lại hãy tham khảo code dưới.

2. Tổ chức chương trình theo giải thuật
Sau khi phân tích và đưa ra giải thuật chúng ta đi vào việc xây dựng chương trình; chúng ta phải viết thủ tục kiểm tra so sánh hai số trừ và số bị trừ; chương trình được thực hiện như sau:
Code:
#include <stdio.h>
#include <conio.h>
int m,n,dem,nho=0,i,a[100],b[100],c[100];
void Input()
{
    printf("Nhap so cac chu so cua a: ");
    scanf("%d",&m);
    printf("\nNhap so a (cac chu so cach nhau boi dau space): ");
    for(i=m-1;i>=0;i--)
        scanf("%d",&a[i]);
    printf("\n\nNhap so cac chu so cua b: ");
    scanf("%d",&n);
    printf("\nNhap so b (cac chu so cach nhau boi dau space): ");
    for(i=n-1;i>=0;i--)
        scanf("%d",&b[i]);
    if(m>n)
        for(i=1;i<=m-n;i++)
            b[n-1+i]=0;
    else
        for(i=1;i<=n-m;i++)
            a[m-1+i]=0;
    if(m<n)
        m=n;
    else n=m; 
}
bool Sosanh()
{
    i=m-1;
    while((a[i]==b[i])&&(i>=0))
        i--;
    if((i<0)||(a[i]>b[i]))
        return true;
    else
        return false;
}
void Tru(int *a,int *b)
{
    int tam;
    if(!Sosanh())
    {
        printf("-");
        for(i=0;i<n;i++)
        {
            tam=a[i];
            a[i]=b[i];
            b[i]=tam;
        }
    }
    for(i=0;i<m;i++)
        if(a[i]-nho>=b[i])
        {
            c[i]=(a[i]-b[i]-nho)%10;
            nho=0;
        } 
        else
        {
            c[i]=(a[i]+10-b[i]-nho)%10;
            nho=1;
        }
        while(c[m-1]==0)
            m--;
        if(m==0)c[m++]=0; 
}
int main()
{
    Input();
    Tru(a,b);
    for(i=m-1;i>=0;i--)
        printf("%d",c[i]);
    getch();
    return 0;
}


---------- Bài thêm lúc 14:33 ---------- Bài trước là lúc 14:29 ----------
Phần 3: Thao tác phép toán Nhân

1. Thuật toán
Giả sử a có m chữ số, b có n chữ số; chúng ta lưu trữ số a và b như sau: Cho a và b có cùng chiều dài là m+n-1 bằng cách thêm vào n-1 số 0 trước a; m-1 số 0 trước b; sau đó a và b được lưu trữ giống như phép toán cộng và trừ; ví dụ a=349, b=35 thì m=3, n=2 và a lúc này có thêm 2-1=1 chữ số 0 đứng trước và trở thành 0349; b tương tự thành 0035; vậy a được lưu trữ thành 9 4 3 0; còn b được lưu trữ là 5 3 0 0.
Vậy tại sao chúng ta lại quy về chiều dài cùng nhau của a và b như vậy? Nếu các bạn để ý thì thấy rằng tích của 2 số có số các chữ số max nhất cũng chính bằng tổng số chữ số của 2 số hạng trừ với 1 nữa, chúng ta quy về như vậy để tiện lợi cho thực hiện các bước tính sau này.
Thuật toán trên được thực hiện qua các bước sau (c=a*b):
- Bước 1: Lưu trữ a và b theo quy tắc trên!
- Bước 2: Khởi tạo nho=0. Tính c[i] và nho theo công thức sau: Đặt s[i]=Tổng j chạy từ 0 đến i của a[j]*b[i-j]; với i là một số nguyên nằm trong đoạn [0,m+n-2].

Code:
c[i]=(s[i]+nho)%10; 
nho=s[i]/10.
Sau m+n-1 bước tính trên mà vẫn còn nhớ thì thêm phần tử nhớ này vào mảng c (và coi là c[m+n-1]).
- Bước 3: Xuất kết quả theo thứ tự ngược lại!
Ví dụ cho dễ hiểu nào?!!! Lấy a=349, b=35.
- Bước 1: Lưu trữ a=9 4 3 0 và b=5 3 0 0.
Vậy a[0]=9; a[1]=4; a[2]=3; a[3]=0 và b[0]=5; b[1]=3; b[2]=0; b[3]=0.
- Bước 2: nho=0. Tính c[i] và nho:
+ s[0]=a[0]*b[0]=9*5=45;
c[0]=(s[0]+nho)%10=(45+0)%10=5;
nho=s[0]/10=4;
+ s[1]=a[0]*b[1]+a[1]*b[0]=9*3+4*5=47;
c[1]=(s[1]+nho)%10=(47+4)%10=1;
nho=s[1]/10=5;
+ s[2]=a[0]*b[2]+a[1]*b[1]+a[2]*b[0]=9*0+4*3+3*5=27;
c[2]=(s[2]+nho)%10=(27+5)%10=2;
nho=s[2]/10=3;
+ s[3]=a[0]*b[3]+a[1]*b[2]+a[2]*b[1]]+a[3]*b[0]=9*0+4*0+3*3+0*5=9;
c[3]=(s[3]+nho)%10=(9+3)%10=2;
nho=s[3]/10=1;
Đến đây thì thuật toán kết thúc; do nho=1 nên ta phải thêm phần tử nhớ này vào mảng c; và c[4]=1.
- Bước 3: Xuất mảng c=5 1 2 2 1; kết quả được sắp xếp lại thành 12215 (đây chính là kết quả của phép nhân 349 với 35)

2. Tổ chức chương trình theo giải thuật
Với việc phân tích thuật toán trên thì sau đây là code của phép toán nhân 2 số nguyên lớn:
Code:
#include <stdio.h> 
#include <conio.h> 
int m,n,chdaichung,nho=0,s,i,j,a[100],b[100],c[100]; 
void Nhan(int *a,int *b) 
{ 
    chdaichung=m+n-1; 
    for(i=0;i<chdaichung;i++) 
    { 
        s=0; 
        for(j=0;j<=i;j++) 
            s+=a[j]*b[i-j]; 
        c[i]=(s+nho)%10; 
        nho=(s+nho)/10; 
    } 
    if(nho>0) 
        c[chdaichung++]=nho; 
} 
void Input() 
{ 
    printf("Nhap so cac chu so cua a: "); 
    scanf("%d",&m); 
    printf("\nNhap so a (cac chu so cach nhau space): "); 
    for(i=m-1;i>=0;i--) 
        scanf("%d",&a[i]); 
    printf("\n\nNhap so cac chu so cua b: "); 
    scanf("%d",&n); 
    printf("\nNhap so b (cac chu so cach nhau space): "); 
    for(i=n-1;i>=0;i--) 
        scanf("%d",&b[i]); 
    while((a[m-1]==0)&&(m>0)) 
        m--; 
    if(m==0) 
        a[m++]=0; 
    while((b[n-1]==0)&&(n>0)) 
        n--; 
    if(n==0) 
        b[n++]=0; 
} 
int main() 
{ 
    Input(); 
    for(i=1;i<=n;i++) 
        a[m-1+i]=0;  
    for(i=1;i<=m;i++) 
        b[n-1+i]=0; 
    Nhan(a,b); 
    for(i=chdaichung-1;i>=0;i--) 
        printf("%d",c[i]); 
    getch(); 
    return 0; 
}


---------- Bài thêm lúc 14:34 ---------- Bài trước là lúc 14:33 ----------
Phần 4: Thao tác toán Chia

1. Thuật toán
Với phép chia thì thuật toán sẽ phức tạp hơn thuật toán của 3 phép toán trên (Cộng, Trừ và Nhân); việc lưu trữ cũng bị biến đổi, cụ thể như sau:
a, Lưu trữ các số hạng tham gia toán chia
Giả sử ta cần kết quả c=a/b; và phép toán này có dư. Các số a, b, c được lưu trữ thành mảng 1 chiều thuận (ngược lại với cách lưu trữ các số hạng của 3 phép toán trước đó), đặc điểm là dùng mảng a để lưu trữ số dư qua từng bước và mảng c để lưu trữ thương!
b, Các khái niệm dùng trong bài toán
Ví dụ một phép chia: 236/13; các bước thực hiện như sau:
- Lấy 23 chia cho 13 (do 2 không chia hết nên phải thêm 3); kết quả được 1 và dư là 10.
- Tiếp tục gán 6 sang 10 ta được 106; chia cho 13 được 8 và dư là 2.
Các số 23 và 106 được gọi là Các phần nhỏ của các bước chia, gọi tắt là Phần nhỏ.
Các số 1 và 2 là các số dư của các Phần nhỏ cho 13 được gọi là các Số dư phần nhỏ.
Ta quy ước một khái niệm gọi là Độ lệch, trong một bước chia mà có k chữ số đầu tiên của a không chia hết cho b thì độ lệch được tăng lên 1 đơn vị, ban đầu chưa chia thì quy ước giá trị của độ lệch là 0. Ký hiệu độ lệch là d; như vậy giả sử a có m chữ số, b có n chữ số, thế thì Độ lệch lớn nhất để chia a cho b là m-n. Khi d>m-n thì phép chia bị kết thúc.
Với ví dụ trên thì số 2 đầu tiên của a không chia hết cho 13 (b) nên độ lệch tăng lên 1, tức d=1; sau khi thêm 3 thì chia được; dư 10 thêm 6 ta vẫn chia được, vậy thuật toán kết thúc và độ lệch d của phép chia luôn là 1.(???).
c, Thuật toán chính
Thuật toán của phép chia cực kỳ phức tạp; có lẽ Peter cũng không thể nêu cụ thể được (các bạn hãy bàn thêm); sau đây là một số nét chính:
- Bước 1: Nếu k chữ số đầu của a nhỏ hơn b thì tăng d lên 1.
- Bước 2: Trong khi d<=m-n thực hiện:
+ Dùng biến Tam (tạm) để lưu trữ thương của các Phần nhỏ cho b; trừ dần ở Phần nhỏ đã xét, lưu số lần trừ được vào biến Tam. Đó là kết quả mỗi chữ số trong thương ở mỗi lần chia.
+ Thêm vào c giá trị của Tam và tăng d lên 1.
+ Nếu a[i]=0 thì tăng i (loại bỏ 1 chữ số 0, không tính đến nó nữa nhưng chú ý là chỉ bỏ 1 chữ số 0, vì nếu còn 1 chữ số nữa thì ở bước tiếp theo Tam=0 và vẫn có kết quả).
- Bước 3: Kiểm tra và xóa các chữ số 0 vô nghĩa ở số dư.
- Bước 4: Xuất kết quả.
Peter không ví dụ thuật toán trên nữa; các bạn hãy tìm hiểu chúng trong đoạn code dưới đây và tham gia thảo luận!

2. Tổ chức chương trình theo giải thuật
Thuật toán trên vẫn còn “mập mờ”! Gây khó hiểu cho nhiều bạn, và đây là code mẫu để các bạn tham khảo thuật toán:

Code:
#include <stdio.h> 
#include <conio.h> 
int m,n,i,j,Tam,d,t,a[100],b[100],c[100]; 
bool duoc; 
bool Sosanh(int *a,int*b,int d) 
{ 
    int i; 
    if((a[d-1]!=0)&&(d>0)) 
        return true; 
    for(i=0;i<n;i++) 
        if(a[i+d]>b[i]) 
            return true; 
        else  
            if(a[i+d]<b[i]) 
                return false; 
        return true; 
} 
void Tru(int *a,int*b,int d) 
{ 
    int i,nho; 
    nho=0; 
    for(i=n-1;i>=0;i--) 
        if(a[i+d]-nho<b[i]) 
        { 
            a[i+d]=a[i+d]+10-nho-b[i]; 
            nho=1; 
        } 
        else 
        { 
            a[i+d]=a[i+d]-nho-b[i]; 
            nho=0; 
        } 
        if(nho==1) 
            a[d-1]--; 
} 
void Ktraso0() 
{ 
    duoc=true; 
    int j=0,k; 
    while((b[j]==0)&&(j<n)) 
        j++; 
    if(j==n) 
        duoc=false; 
    else  
    { 
        for(k=j;k<n;k++) 
            b[k-j]=b[k]; 
        n-=j; 
    } 
    j=0; 
    while((a[j]==0)&&(j<m)) 
        j++; 
    if(j==m) 
    { 
        m=1;a[0]=0; 
    } 
    else  
    { 
        for(k=j;k<m;k++) 
            a[k-j]=a[k]; 
        m-=j; 
    } 
} 
void Chia(int *a,int *b) 
{ 
    if(m>=n) 
    { 
        if(!Sosanh(a,b,d)) 
            d++; 
        while(d<=m-n) 
        { 
            Tam=0; 
            while(Sosanh(a,b,d)) 
            { 
                Tam++; 
                Tru(a,b,d); 
            } 
            c[t++]=Tam; 
            d++; 
            if(a[i]==0) 
                i++; 
        } 
        if(t==0) 
            c[t++]=0; 
    } 
    else  
        c[t++]=0; 
    while((a[i]==0)&&(i<m)) 
        i++; 
    if(i==m) 
        a[--i]=0; 
} 
int main() 
{ 
    printf("Nhap so cac chu so cua a: "); 
    scanf("%d",&m); 
    printf("\nNhap so a (cac chu so cach nhau bang space): "); 
    for(i=0;i<m;i++) 
        scanf("%d",&a[i]); 
    printf("\n\nNhap so cac chu so cua b: "); 
    scanf("%d",&n); 
    printf("\nNhap so b (cac chu so cach nhau bang space): "); 
    for(i=0;i<n;i++) 
        scanf("%d",&b[i]); 
    d=0; 
    i=0; 
    t=0; 
    Ktraso0(); 
    if(duoc) 
    { 
        Chia(a,b); 
        printf("\nThuong cua phep chia la: "); 
        for(j=0;j<t;j++) 
            printf("%d",c[j]); 
        printf("\nSo du cua phep chia: "); 
        for(j=i;j<m;j++) 
            printf("%d",a[j]); 
    } 
    else  
        printf("Chia cho 0!!!! Vui long lam lai...."); 
    getch(); 
    return 0; 
}

(tác giả:
peterdrew
nguồn: congdongCviet)
Read More...

Thứ Năm, 28 tháng 7, 2011

Thuật toán Knuth-Morris-Pratt

+ Tác giả: Chuyên gia khoa học máy tính NQH

+ Đặt vấn đề: Pattern matching là một trong những bài toán cơ bản và quan trọng nhất của ngành máy tính.

Tìm patterns trong các chuỗi-DNA là bài toán cơ bản của sinh tin học. Các phần mềm quét virus hiện đại có mấy chục triệu “patterns” là các “chữ ký” (virus signature) của các con virus máy tính đã biết. Khi quét máy thì phần mềm phải tìm các patterns này trong các files hay bộ nhớ của máy. Mỗi pattern thường là một chuỗi bytes nhất định. Lệnh grep chúng ta thường dùng trong Unix cũng làm tác vụ tương tự, trong đó các patterns có thể được biểu diễn bằng các biểu thức chính quy (regular expressions).

Nếu chỉ tính các thuật toán tìm các xuất hiện của một chuỗi đơn (một chuỗi các ký tự cho sẵn) bên trong một chuỗi khác thôi thì ta đã có đến cả trăm thuật toán khác nhau. Knuth-Morris-Pratt (KMP) là một trong những thuật toán đó. KMP không phải là thuật toán nhanh nhất hay tốn ít bộ nhớ nhất trên thực tế. Trên thực tế các biến thể của thuật toán Boyer-Moore hay Rabin-Karp với hàm băm và bộ lọc Bloom thường được dùng. Ta sẽ nói về chúng sau. Nhưng KMP thật sự rất thanh lịch và nếu tác vụ tìm kiếm không ghê gớm quá thì KMP không kém Boyer-Moore là mấy.

Ý tưởng của KMP rất đơn giản. Nếu bạn đọc nó trong quyển CLRS (Introduction to Algorithms) thì bạn sẽ bị lạc trong sa mạc. Thật sự là CLRS làm cho mọi thứ phức tạp hơn cần thiết. Quá nhiều ký hiệu và quá ít trực quan. Ta sẽ thảo luận KMP dùng 2 bước. Bước 1 là thuật toán Morris-Pratt (1970, A linear pattern-matching algorithm, Technical Report 40, UC Berkeley). Bước 2 là một cải tiến của thuật toán MP, có thêm Knuth vào (1977, Fast pattern matching in strings, SIAM Journal on Computing 6(1):323-350).

Mặc dù hai bài báo cách nhau 7 năm, thật ra là thuật toán này đã được khám phá từ khoảng 1969 với một lịch sử thú vị. Phần cuối bài báo của KMP mô tả chi tiết lịch sử này. Cả phần kỹ thuật lẫn lịch sử trong bài báo đều rất chi tiết, kiểu Knuth, đọc rất thích.

1. Thuật toán Morris-Pratt
MP là thuật toán thời gian O(n) đầu tiên cho bài này. Ý tưởng của thuật toán MP là như sau. Giả sử ta muốn tìm pattern p[0..m-1] trong chuỗi s[0..n-1]. Đến một lúc nào đó thì ta có mis-match: s[j] != p[i] như trong hình sau:



Nếu dùng thuật toán cơ bắp (điều mà bạn nên làm khi đi phỏng vấn người ta hỏi viết strstr() lên bảng) thì ta dịch p một vị trí và làm lại từ đầu. Thời gian chạy sẽ là O(mn). Tồi! Nhưng mà làm lại từ đầu thì rất phí công chúng ta đã so sánh đến s[j]. Ta tìm cách dịch p đi xa hơn. Càng xa càng tốt miễn là không bị lố qua một xuất hiện của pattern trong chuỗi s. Dễ thấy rằng, để giữ vị trí của j không đổi thì ta phải dịch p đi một đoạn sao cho một tiền tố (prefix) của p[0..i-1] được xếp trùng bằng một hậu tố (suffix) của p[0..i-1], tại vì đoạn hậu tố này đã được so trùng với đoạn hậu tố cùng chiều dài của s[0..j-1]. Khi đó, ta chỉ cần tiếp tục so sánh s[j] với p[map[i]] mà không cần làm lại từ đầu. Trong đó, map[i] < i chỉ chỗ cho ta biết xê dịch p như thế nào.

Chiều dài của sự xê dịch (bằng với đại lượng i - map[i]) được gọi là một chu kỳ (period) của chuỗi p[0..i-1]. Phần tiền tố và hậu tố trùng khớp với nhau được gọi là biên (border) của chuỗi p[0..i-1]. Chiều dài của biên bằng i trừ đi chu kỳ. Để cho sự xê dịch có nghĩa, chu kỳ phải lớn hơn 0 và vì thế biên của p[0..i-1] luôn có chiều dài nhỏ hơn i. Để tránh trường hợp bị bỏ sót một xuất hiện của p trong s thì ta phải chọn biên dài nhất của p[0..i-1], ký hiệu là border(p[0..i-1]).

Trong thuật toán MP ta tính trước dãy map. Sau đó dùng dãy map này để so sánh hai chuỗi. Chặt chẽ hơn, với một chuỗi x bất kỳ, định nghĩa w = border(x) là chuỗi w dài nhất thỏa mãn 0 ≤ |w| < |x| sao cho w vừa là tiền tố vừa là hậu tố của x, nghĩa là x = wy = zw, trong đó y, z là các chuỗi nào đó. Lưu ý rằng 0 < |y| = |z| = period(x).

Bây giờ giả sử ta đã có bảng map[0...m], trong đó map[0] = -1 và map[i] = |border(p[0..i-1])| với 1 ≤ i ≤ m. Thuật toán Morris-Pratt có thể được viết như sau:


Python Code:

def MP(s, p): # print all occurrences of pattern p in string s
map = compute_MP_map(p)
i = 0
n = len(s); m = len(p)
for j in range(n):
while ( (i >= 0) and (s[j] != p[i]) ):
i = map[i]
i = i+1
if (i == m):
print "Match at position ", j-m+1
i = map[i]

Ta phân tích thời gian chạy của MP dùng phương pháp phân tích khấu hao (Amortized Analysis). Ta cho mỗi chú s[j] 2 đồng xu để dùng. Và tưởng tượng bọn p[i] là m cái rọ. Lúc đầu bỏ vào rọ p[0] một đồng xu làm vốn. Mỗi lần phép so sánh ký tự ở dòng 6 được chạy thì ta dùng đồng xu có sẵn trong rọ p[i] để trả nợ cho phép so sánh này. (Lưu ý rằng ta giả sử nếu i=-1 thì không có phép so sánh. Nếu bạn cẩn thận bạn có thể bẻ đôi điều kiện của while ra để tránh truy cập p[-1]. Đoạn Python trên tôi đã chạy, không có vấn đề gì.) Xong rồi đến cuối, ngay trước khi thực hiện phép gán i = i+1 ở dòng 8 ta lấy 2 đồng xu của s[j] bỏ vào p[i] và p[i+1]. Như vậy thời gian chạy của MP là O(n), và nó chỉ dùng nhiều nhất 2n phép so sánh.

Vấn đề tiếp theo là làm sao tính map. Đây là một bài toán quy hoạch động hay. Lưu ý rằng biên của biên của một chuỗi x cũng là biên của x. Giả sử ta muốn tính map[i+1] = |border(p[0..i])|. Dễ thấy rằng, nếu p[i] = p[map[i]] thì map[i+1] = map[i] + 1. Nếu p[i] != p[map[i]] thì ta so p[i] với p[map[map[i]]], vân vân. Từ đó ta có đoạn mã sau:

Python Code:

def compute_MP_map(p):
m = len(p) # p is the input pattern
map = [-1]*(m+1) # map[0..m], the Morris-Pratt border map
i = 0; j = map[i]
while (i < m):
while ( (j >= 0) and (p[i] != p[j]) ):
j = map[j]
j = j+1; i = i+1
map[i] = j
return map

Bài tập: dùng phương pháp phân tích khấu hao tương tự như trên, chứng minh rằng thời gian chạy của compute_MP_map() là O(m).

Chạy thử:

C++ Code:

>>> MP("abcabcabcabc", "cabc")
Match at position 2
Match at position 5
Match at position 8

2. Thuật toán Knuth-Morris-Pratt

KMP cải tiến map một chút. Trong hình ở trên, nếu p[map[i]] = b thì ta lại có mis-match. Do đó, ta áp đặt một cú "dòm trước" (look ahead) khi tính map. Gọi MPmap là dãy map của thuật toán MP. Cái map cải tiến của thuật toán KMP được định nghĩa như sau. KMPmap[0] = -1. Với 1 ≤ i ≤ m-1 thì gọi j = MPmap[i]. Khi đó, nếu p[i] != p[j] thì KMPmap[i] = j. Nếu p[i] == p[j] thì KMPmap[i] = KMPmap[j]. Và cuối cùng, KMPmap[m] = MPmap[m]. Bạn nên nghĩ cẩn thận xem tại sao định nghĩa KMPmap như vậy là hữu lý.

Dĩ nhiên, nếu đã tính MPmap rồi thì tính KMPmap theo công thức trên dễ dàng thôi. Nhưng ta có thể viết hàm tính KMPmap trực tiếp. Đây là một bài tập lập trình rất hay! Trong Python có thể viết như sau:

Python Code:

def compute_KMP_map(p):
m = len(p) # p is the input pattern
map = [-1]*(m+1) # map[0..m], the Knuth-Morris-Pratt border map
i = 1; map[i] = 0; j = map[i]
while (i < m):
# at this point, j is MP_map[i], which is not necessarily KMP_map[i]
if (p[i] == p[j]):
map[i] = map[j]
else:
map[i] = j
while ( (j >= 0) and (p[i] != p[j]) ):
j = map[j]
j = j+1; i = i+1
map[m] = j
return map

def KMP(s, p): # print all occurrences of pattern p in string s
map = compute_KMP_map(p)
i = 0; j = 0
n = len(s); m = len(p)
for j in range(n):
while ( (i >= 0) and (s[j] != p[i]) ):
i = map[i]
i = i+1
if (i == m):
print "Match at position ", j-m+1
i = map[i]

>>> KMP("abcabcabcabc", "cabc")
Match at position 2
Match at position 5
Match at position 8

Điểm thú vị cuối cùng là, mỗi ký tự của chuỗi s được so sánh với nhiều nhất là (đại lượng này gọi là delay của thuật toán), trong đó là tỉ lệ vàng!

+ Code hoàn chỉnh:

Visual C# Code:

using System;

namespace Knuth_Morris_Pratt
{
class Test
{
private char[] s;
private char[] p;

public void Nhap()
{
Console.Write("Nhap xau S:");
string stdin1 = Console.ReadLine();
s = new char[stdin1.Length];
s = stdin1.ToCharArray();
Console.Write("Nhap xau P:");
string stdin2 = Console.ReadLine();
p = new char[stdin2.Length];
p = stdin2.ToCharArray();
}

private int[] compute_MP_map() // map[i]=| border(p[0..i-1]) |
{
int m = p.Length; // p is the input pattern
int[] MP_map = new int[m + 1]; // lưu ý rằng ta phải tính map[0..m]
// init
MP_map[0] = -1; // cái này chắc ai cũng hiểu
// bay gio di tinh map[1..m]
int i = 0; int j = MP_map[i];
while (i < m)
{
while (j >= 0 && (p[i] != p[j])) j = MP_map[j];
j++; i++;
MP_map[i] = j;
}
return MP_map;
}

private int[] compute_KMP_map() //thuật toán KMP cải tiến
{
int m = p.Length;
int[] KMP_map = new int[m + 1];
int[] MP_map = compute_MP_map();
KMP_map[0] = -1; KMP_map[m] = MP_map[m];
for (int i = 1; i < m; i++)
{
int j = MP_map[i];
if (p[i] != p[j]) KMP_map[i] = j;
else KMP_map[i] = MP_map[j];
}
return KMP_map;
}

public void Morris_Pratt()
{
int[] map = compute_KMP_map();
int n = s.Length;
int m = p.Length;
int i = 0;
string res = "";
for (int j = 0; j < n; j++)
{
while ((i >= 0) && (p[i] != s[j])) i = map[i];
i++; // Có 2 khả năng xảy ra: hoặc là đã đi hết chuỗi p (i=m-1) hoặc là i=-1 , lợi dụng cả 2 điều này
if (i == m)
{
res += (j - m + 1).ToString() + " ";
i = map[i];
}
}
Console.Write(res);
}


static void Main(string[] args)
{
Test object1 = new Test();
object1.Nhap();
object1.Morris_Pratt();

//Console.WriteLine("done");
Console.ReadLine();
}
}
}



Read More...

Chủ Nhật, 24 tháng 7, 2011

Một số thuật toán cơ bản


 

Kiểm tra 1 số nguyên tố

 + Định nghĩa: Là số nguyên lớn hơn 1, chỉ có 2 ước là 1 và chính nó. Các số nguyên tố từ 1-100: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97

+ Thuật toán: Để kiểm tra 1 số nguyên n có phải số nguyên tố hay không, ta làm theo các bước.
- Nếu n<2 thì không phải số nguyên tố.
- Kiểm tra trong đoạn từ 2..sqrt(n) xem có ước của n không, nếu có tồn tại thì n không phải số nguyên tố
- Ngược lại, n là số nguyên tố.

 

 
Phương pháp sàng Eratosthene

 + Mục đích: Để lập bảng các số nguyên tố nhỏ hơn hoặc bằng 1 số n cho trước.

+ Thuật toán: Sử dụng 1 bảng bool isPrimeNumber[0..n+1] để lưu kết quả
- Khởi tạo: tất cả các số từ 1->n là nguyên tố.
- Xóa số 1 ra khỏi bảng.
- Lặp: Tìm 1 số nguyên tố đầu tiên trong bảng sau đó xóa tất cả các bội của nó trong bảng.
- Quá trình lặp kết thúc khi gặp số nguyên tố >= sqrt(n).
- Tất cả các số chưa bị xóa trong bảng là số nguyên tố.

 

Tìm ước số chung lớn nhất (GCD)

+ Định nghĩa: Ước số chung lớn nhất (Greatest Common Divisor) của 2 số a và b, được định nghĩa như sau: gcd(a,b)=d <=> d là số lớn nhất trong tất cả các ước chung của a và b.

+ Thuật toán: Giải thuật Euclid, định nghĩa đệ qui như sau:
- gcd(a,0)=a
- gcd(a,b)=gcd(b,a mod b)

 

 

Tìm bội số chung nhỏ nhất (lcm)

 + Định nghĩa: Bội số chung nhỏ nhất (Least Common Multiple) của 2 số a và b được định nghĩa LCM(a,b)=m <=> m là số nhỏ nhất trong tất cả các bội chung của a và b.

+ Thuật toán: Ta có công thức:

 

 

Tính n giai thừa (Factorial)

 + Công thức:

 

 Phát biểu bằng lời: Giai thừa của 1 số nguyên dương n là tích tất cả các số nguyên dương nhỏ hơn hoặc bằng n.

 + Thuật toán: Ta có thuật toán đệ qui như sau:

 

 
Tính tổ hợp chập k của n

 + Công thức:

  

 + Thuật toán:
- Trâu bò (Brute Force): sử dụng hàm tính giai thừa.

- Sử dụng vòng lặp:

 

Kiểm tra số chính phương

 + Định nghĩa: Số chính phương (SquareNumber) là số có căn bậc 2 là 1 số nguyên. Ví dụ: 4,9,100...

+ Thuật toán: Để kiểm tra n có phải số chính phương hay không ta lấy phần nguyên của căn bậc 2 của n rồi bình phương, sau đó so sánh với n.
Ví dụ: sqrt(4)=2.0 Ta thấy 2^2=4 => 4 là số chính phương
sqrt(7)=2.4657 , phần nguyên là 2. Ta thấy 2^2 <> 7 => 7 không phải số chính phương.


Tìm kiếm nhị phân (Binary Search)

 + Sơ lược: Tìm kiếm nhị phân là thuật toán nhằm xác định vị trí của 1 phần tử trong 1 mảng đã được sắp xếp. Thuật toán hoạt động dựa trên việc so sánh giá trị phần tử đầu vào với phần tử ở giữa (middle) của dãy. Việc so sánh sẽ cho biết phần tử ở giữa này bằng, nhỏ hơn hay lớn hơn giá trị đầu vào. Khi phần tử được đem so sánh mà bằng input thì việc tìm kiếm sẽ kết thúc và trả về vị trí của phần tử đó. Nếu phần tử này nhỏ hơn input thì đồng nghĩa với việc ta cần tìm input trong đoạn từ middle+1..top, ngược lại nếu lớn hơn input thì ta cần tìm input trong đoạn bottom..middle-1. Khi dãy tại bước hiện tại không có phần tử nào (bottom>top) thì không cần tìm kiếm. Kết thúc quá trình tìm kiếm nếu không thấy input xuất hiện trong dãy thì kết quả trả về -1.

+ Code:Visual C# Code:

public
int BinarySearch(int[] a, int value, int bottom, int top)
{
while (bottom <= top) // trong khi dãy đang xét còn phần tử
{
int middle = (bottom + top) / 2;
if (a[middle] == value) return middle; // đã tìm thấy tại vị trí middle, quá trình tìm kiếm dừng.
else if (a[middle] > value) top = middle - 1; // cần tìm value trong đoạn bottom..middle-1
else bottom = middle + 1; // cần tìm value trong đoạn middle+1..top
}
return -1; //Không tìm thấy
}


Chú ý: 
+ Độ phức tạp của thuật toán là O(log(n))
+ Thông thường giá trị bottom và top ban đầu tương ứng với 0 và N-1.



Số hoàn hảo (Perfect Number)

 + Định nghĩa: Số hoàn hảo là số có tổng các ước nhỏ hơn nó bằng chính nó. Ví dụ: 6 = 1+2+3. 28 = 1+2+4+7+14.

+ Thuật toán: Để kiểm tra n có phải Perfect Number hay không, đi tìm tổng tất cả các ước nhỏ hơn n rồi so sánh.

 

 

Kiểm tra số nguyên tố cùng nhau

 + Đặt vấn đề: Trong khi thao tác với các con số nhiều lúc các bạn gặp phải cụm từ hai số nguyên tố cùng nhau!; lúc đó có bạn nghĩ rằng đó phải là 2 số nguyên tố gần nhau. Hì, sai mất rồi đó; vậy nên trong Vấn đề 5 Peter đã khái quát qua (dẫn các bạn trước khi vào vấn đề này) là: Hai số nguyên dương a và b được gọi là nguyên tố cùng nhau khi và chỉ khi Ước chung lớn nhất của chúng là 1; tức là (a,b)=1; chứ không hẳn a và b phải là 2 số nguyên tố đứng gần nhau (hoặc có một cái hiểu tương tự), dĩ nhiên nếu a và b là hai số nguyên tố khác nhau thì chúng cũng thoả mãn tính chất là hai số "nguyên tố cùng nhau" (Lẫn lộn quá, nhưng các bạn chú ý cho điều này).

+ Phương pháp: Vậy làm sao để kiểm tra chúng nguyên tố cùng nhau hay không? Rất đơn giản là (ký hiệu hàm là CoPrime()):
- Nếu ước chung lớn nhất của chúng khác 1 thì trị trả về cho hàm kiểm tra là 0, báo hiệu hai số không nguyên tố cùng nhau.
- Ngược lại, thì trị trả về cho hàm kiểm tra là 1, báo hiệu hai số nguyên tố cùng nhau.

 

 

Số các ước số nguyên dương của một số nguyên dương

 + Đặt vấn đề: Trong toán lý thuyết số thì vấn đề về liệt kê các ước nguyên dương của một số nguyên dương chiếm một vị trí cũng khá quan trọng, số các ước số nguyên dương của một số nguyên dương n được cho bởi công thức sau:Tên:  mimetex.gif
Lần xem: 	129
Size:  		518 Bytes
Các bạn thường gặp một bài toán phổ biến là Phân tích một số ra tích các thừa số nguyên tố (mà ngày xưa học lớp 6 Peter mới được tiếp cận); nhìn thấy các bạn giải quyết chúng khá đơn giản bằng các vòng lặp for. Đó là:

+
 Code:
C++ Code:
int NoOfDivisor(int n)
{
int dem=0;
for (int i=1;i<=(int)sqrt(n);i++)
if (n%i==0)
dem++;
return dem;
}

 


 

 

Read More...