Chuyển đến nội dung chính

Tính a*b theo lũy thừa nhị phân

Như ta đã biết với a và b là 2 số nguyên 

  •         a*b = a*(b-1)+a  khi b>0

cách làm này cũng vẫn sẽ mất  b bước thực hiện. Tượng tự ta có tính chất sau trong phép nhân

  •         a*b=a*(b/2)+a*(b/2)

Từ đó: ta có thể phân tích a*b như sau:

  • a*b = a*(b/2)+a*(b/2)               khi b chẵn
  • a*b= a*(b/2) + a*(b/2)+a          khi b lẻ
Với tính chất này tương tự như lũy thừa ta có thể tính a*b bằng cách tính a*(b/2) mà khong cần tính 
a*(b-1)+a... từ đó ta có tính a*b với độ phức tạp là O(log(n))

int bin_multiplication(int a, int b)
{
    int res = 0;
    while (b > 0) {
        if (b % 2 == 1)
            res = res + a;
        a= a + a;
        b = b >> 1; // b=b/2
    }
    return res;
}


- Đối với cách làm này ta không chỉ tính toán a*b với độ phức tạp là O(log(n)) mà ta có thể thông qua cách làm này để xử lý khi lấy 2 số lớn nhân nhau và mod cho một số m
-Ắt hản chúng ta đã từng gặp một số bài toán yêu cầu modulo đáp án cho một số m thông thường m = 1e9+7.
-VD cụ thể ta có bài toán (a*b)%m với :
  • a=1e13+3
  • b=1e12+7
  • m = 1e9+7
-Nếu ta lấy trực tiếp (a*b)%m thì kết quả sẽ lập tức bị tràn khỏi kiểu dữ liệu int hay thậm chí là kiểu dữ liệu long long. Tất nhiên vẫn sẽ có thể sử dụng công thức mod
  •    (a*b)%m = (a%m*b%m)%m
-Điều này sẽ làm giảm kích thước a và b trước khi nhân để a và b có thể nhân nhau mà không vượt ngoại phạm vi kiểu dữ liệu long long trước khi mod
-Tuy nhiên trong một số bài toán số m có thể đạt tới 1e18 vì lẻ đó nếu ta áp dụng công thức trên thì 

  • a%m  vẫn sẽ bằng 1e13+3
  • b%m  vẫn sẽ bằng 1e12+7
  • với m = 1e18
-Kết quả vẫn sẽ chăng thay đổi gì vì a và b đều bé hơn số m nên khi (a*b)%m vẫn sẽ bị tràn số
-Khi đó ta sẽ áp dụng kỹ thuật tính a*b theo lũy thừa nhị phân (bin_multiplication)
long long  bin_multiplication(long long  a, long long  b, long long m)
{
    long long  res = 0;
    while (b > 0) {
        if (b % 2 == 1)
            res = res + a;
            res = res % m;
        a= a + a;
        a= a % m ;
        b = b >> 1; // b=b/2
    }
    return res;
}

- Với cách làm này ta vẫn sẽ đảm bảo (a*b) %m sẽ không bị tràn số khi m đạt tới 1e18 mà vẫn đảm bảo độ phức tạp thuật toán trong thời gian cho phép đủ dư giả để giải quyết các bài toán yêu cầu 1s

Nhận xét

Bài đăng phổ biến từ blog này

Thuật toán lùa bò vào chuồng(Đếm phân phối)

Làm cách nào từ một coder chân chính  trở thành một người chăn bò 😃😃 Chắc hẳn trong lập trình chúng ta không khó để gặp gỡ các bài toán đếm.Tuy nhiên việc thực hiện các bài toán đếm thường được diễn ra trên dữ liệu lớn, nếu các bạn không biết cách tổ chức dữ liệu và thực hiện  thuật toán hiệu quả thường sẽ dẫn đến kết quả sai lầm. Vì vậy trong bài viết hôm nay chúng ta sẽ cùng tìm hiểu qua một trong những cách tiếp cận bài toán đếm đó là thuật toán lùa bò vào chuồng hay còn gọi là đếm phân phối. 1. Đặt vấn đề: Ban đầu bạn có một mảng số gồm N phần tử ( N <10^5) mỗi giá trị trong mảng đều bé hơn hoặc bằng 100. Bạn được giao nhiệm vụ đếm xem trong mảng số đó có bao nhiêu phần tử riêng biệt.      VD: N=10      arr []  =   1 6 9 1  7 9 9 9 2 3 => 6 phần tử riêng biệt. 2.Thuật toán lùa bò vào chuồng:   Tư tưởng của thuật toán: giả sử bạn là một người nông dân sở hữu một nông trại bò và bạn cần phải đếm chính xác s...

Vét cạn hay duyệt trâu (Brute Force)

Thuật toán vét cạn:  Thuật toán vét cạn hay duyệt trâu (Brute Force) được  hiểu đúng như tên gọi của nó, chúng ta sẽ dùng những phương pháp đơn giản để duyệt qua toàn bộ các trường hợp của bài toán và bằng sức mạnh của máy  tính để tìm ra được đáp án chính xác thay vì dùng các thuật toán hiệu quả hơn .  VD: bài toán sống sót qua cuối tháng khi hết tiền tiêu:  Giải pháp của bài này là chúng ta sẽ dùng  vòng for vét can toàn bộ lương thực quanh nhà như mì tôm, hủ gạo... Để có thể tìm ra bữa ăn sống sót qua ngày và vì phải chạy đi tìm kiếm khắp nơi nên cách làm này sẽ có hơi tốn sức (dẫn đến chết đói). Có nhiều cách duyệt khác nhau như: Dùng nhiều vòng for If, else Đệ Quy, quay lui (Backtracking) Bitmask .... Giải thuật này thường rất hiệu quả với những bài toán có dữ liệu đầu vào nhỏ nhưng đối với các dữ liệu lớn hơn hay các bài toán phức tạp hơn sẽ tốn rất nhiều thời gian và đòi hỏi một lực code trâu bò để có thể vét cạn hết toàn bộ.

Đệ quy

  Đệ quy Hãy nhìn vào tấm hình sau Phía bên trong bức ảnh chứa một phiên bản của chính nó, và bên trong phiên bản đó, tiếp tục chứa một phiên bản nữa của chính nó. Điều này được lặp đi lặp lại, cho chúng ta một khái niệm trực quan về đệ quy. Ví dụ toán học về đệ quy Cho hàm  f ( n )  được định nghĩa như sau: f ( n ) = 1 + 2 + 3 + . . . + n Đồng nghĩa với:  f ( n − 1 ) = 1 + 2 + 3 + . . . + n − 1 ⇒ f ( n ) = n + f ( n − 1 ) Tất nhiên ta có thể tính biểu thức trên bằng công thức toán học  f ( n ) = n × ( n + 1 ) 2 , nhưng tạm thời ta không xét nó ở đây. Qua chứng minh trên ta rút ra được 1 điều: để tính  f ( n ) , ta có thể tính từ  f ( n − 1 ) . Vậy làm sao để tính  f ( n − 1 ) ? Tương tự, ta có thể tính nó từ  f ( n − 2 ) , và rồi ta sẽ tính  f ( n − 2 )  từ  f ( n − 3 ) , ... Bên trong bài toán  trên chứa một phiên bản nhỏ hơn của chính nó, trong tin học, khái niệm này gọi là đệ quy. Tuy nhiên, nếu chỉ tìm đáp án từ biểu ...