APIO10A – spoj

Đề bài: http://www.spoj.com/problems/APIO10A/


Cho 1 dãy N số nguyên. Một hàm số bậc 2 : f(x) = a * x ^ 2 + b * x + c. Phân dãy trên thành các đoạn liên tiếp sao cho tổng các hàm f trên các dãy là lớn nhất (giá trị f của 1 dãy là f(x) với x là tổng của dãy đó).

Input format :

  • Dòng đầu là số test case T
  • Mỗi test case gồm 3 dòng :
    • Dòng đầu là số nguyên dương N – số phần tử của dãy.
    • Dòng 2 là 3 số nguyên a, b, c.
    • Dòng còn lại gồm n số x1, x2, …, xn là n phần tử của dãy.

Output format :

  • Mỗi test case gồm 1 dòng, là kết quả của bài toán.

Giới hạn :

T<=3

n ≤ 1, 000, 000,

−5 ≤ a ≤ −1

b <= 10,000,000

c <= 10,000,000

1 ≤ xi ≤ 100.

Thuật toán:


Gọi f(x) = a * x ^ 2 + b * x + c

Thuật O(n^2):

Gọi dp(i) là chi phí lớn nhất khi phân hoạch đoạn từ 1 -> i.

sum(i) là tổng các phần tử từ 1 -> i.

dp(i) = max(dp(j) + f(sum(i) – sum(j)) (1 <= i <= n; 0 <= j < i)

 

Thuật O(n): dùng Convex Hull Trick

dp(i) = max(dp(j) + f(sum(i) – sum(j)) (1 <= i <= n; 0 <= j < i)

⇔ dp(i) = dp(j) + a * (sum(i) – sum(j))^ 2 + b * (sum(i) – sum(j)) + c

⇔ dp(i) = (a * sum(i) ^ 2 + b * sum(i) + c) + (-2 * a * sum(i) * sum(j)) + a * sum(j) ^ 2 – b * sum(j) ^ 2

Đặt A = -2 * a * sum(j), X = sum(i), B = a * sum(j) ^ 2 – b * sum(j) ^ 2

⇔ ta được đường thẳng y = A * X + B.

Vì mảng sum tăng dần -> ta có thể dùng two-pointer để giảm đpt xuống O(n)

Code:


#include 
using namespace std;

const int N = 1e6 + 10;

class ConvexHull {
private:
    int head, tail;
    long long A[N], B[N];
public:
    void init() { head = tail = 0; }
    bool bad(int l1, int l2, int l3) {
        return (long double) (B[l3] - B[l1]) / (A[l1] - A[l3]) < (long double) (B[l2] - B[l1]) / (A[l1] - A[l2]);
    }
    void add(long long a, long long b) {
        A[tail] = a; B[tail++] = b;
        while (tail > 2 && bad(tail - 3, tail - 2, tail - 1)) {
            A[tail - 2] = A[tail - 1];
            B[tail - 2] = B[tail - 1];
            tail--;
        }
    }
    long long query(long long x) {
        if (head >= tail) head = tail - 1;
        while (head < tail - 1
               && A[head + 1] * x + B[head + 1] >= A[head] * x + B[head]) head++;
        return A[head] * x + B[head];
    }
} hull;

int n, a, b, c;
long long sum[N];

long long f(long long x) { return a * x * x + b * x + c; }

void load() {
    scanf("%d%d%d%d", &n, &a, &b, &c);
    for (int i = 1; i <= n; ++i) {
        scanf("%lld", sum + i);
        sum[i] += sum[i - 1];
    }
}

void process() {
    hull.init();
    long long cost = f(sum[1]);
    hull.add(-2 * a * sum[1], cost + a * sum[1] * sum[1] - b * sum[1]);

    for (int i = 2; i <= n; ++i) {
        cost = f(sum[i]) + max(0ll, hull.query(sum[i]));
        hull.add(-2 * a * sum[i], cost + a * sum[i] * sum[i] - b * sum[i]);
    }
    printf("%lld\n", cost);
}

int main() {
  //  freopen("input.in", "r", stdin);
 //   freopen("output.out", "w", stdout);

    int test; scanf("%d", &test);
    while (test--) {
        load();
        process();
    }

    return 0;
}

NKPATH – spoj

[mathjax]

Đề bài:


Thuật toán:


Với mỗi ô (i,j) với  $i=1..m; j=1..n-1;$ kiểm tra xem ô (ii,jj) với $ii=1..i-1; jj=1..j;$ có đi được đến ô (i,j) không

  • Nếu có
    l[i,j]:=(l[i,j]+l[ii,jj]) mod base;
    

Kết quả: $\sum_{i=1}^{m}L[i][n]$

Code:


const   fi='';
        fo='';
        maxn=100;
        base=1000000000;
type    arra=array[1..maxn,1..maxn] of integer;
        arrl=array[1..maxn,1..maxn] of longint;
var     a:arra;
        i,j,m,n:byte;
        l:arrl;
        f:text;
        res:int64;
procedure nhap;
begin
    assign(f,fi);
    reset(f);
    readln(f,m,n);
    for i:=1 to m do
        for j:=1 to n do read(f,a[i,j]);
    close(f);
end;
procedure init;
begin
    for i:=1 to m do l[i,1]:=1;

    res:=0;
end;
function kt(x,y:longint):boolean;
var     tmp:longint;
begin
        while y>0 do
        begin
            x:=x mod y;
            tmp:=x;
            x:=y;
            y:=tmp;
        end;
        if x=1 then exit(false) else exit(true);
end;
procedure xuly;
var     ii,jj:byte;
begin
    for i:=1 to m do
        for j:=1 to n-1 do
        begin
            for ii:=i-1 downto 1 do
                for jj:=j downto 1 do
                        if kt(a[i,j],a[ii,jj]) then l[i,j]:=(l[i,j]+l[ii,jj]) mod base;
            for jj:=j-1 downto 1 do
                if kt(a[i,j],a[i,jj]) then l[i,j]:=(l[i,j]+l[i,jj]) mod base;
        end;

    for i:=1 to m do
        for ii:=i downto 1 do
                for jj:=n-1 downto 1 do
                if kt(a[i,n],a[ii,jj]) then res:=(res+l[ii,jj]) mod base;
end;
procedure xuat;
begin
    assign(f,fo);
    rewrite(f);
    writeln(f,res);
    close(f);
end;
begin
    nhap;
    init;
    xuly;
    xuat;
end.

C11BC1 – spoj

Đề bài:


Thuật toán:


  • (đang cập nhập)

Code:


uses math;
const
  fi='';
  fo='';
  maxn=trunc(1e5);
  maxk=50;
  base=790972;
var
  f : array[0..maxn,0..maxk] of int64;
  i,j,n,k,m : longint;
  kq : int64;
  a,b : array[1..maxn] of longint;
procedure enter;
begin
  assign(input,fi);reset(input);
  readln(n,k);
  for i:=1 to n do read(a[i],b[i]);
  close(input);
end;
procedure swap(var x,y: longint);
var tg : longint;
begin
  tg:=x;x:=y;y:=tg;
end;
procedure qs(l,r : longint);
  var i,j,x : longint;
  begin
    i:=l;j:=r;
    x:=b[(l+r) div 2];
    repeat
      while x>b[i] do inc(i);
      while xj;
    if il then qs(l,j);
  end;
function muk(x : longint) : int64;
  var i : longint;
  begin
    muk := 1;
    for i:=1 to k do muk:=muk*x;
  end;
function cnk(n,k : longint) : int64;
  begin
    cnk := 1;
    for i:=n-k+1 to n do cnk := cnk*i;
    for i:=1 to k do cnk := cnk div i;
  end;
function tinh : int64;
begin
  fillchar(f,sizeof(f),0);
  for i:=0 to m do f[i,0] := 1;
  for i:=1 to m do
    for j:=1 to min(k,i) do
      f[i,j] := (f[i-1,j] + f[i-1,j-1]*a[i]) mod base;
  exit(f[m,k]);
end;
procedure process;
var i,j,dem : longint;
    dau,cuoi : longint;
begin
  qs(1,n);
  m := n;
  kq := tinh;
  dau := 1;
  while (dau<=n) do
  begin
    cuoi := dau+1;
    m := 1; a[1] := a[dau];
    while (cuoi<=n) and (b[cuoi]=b[dau]) do
      begin
        m := m+1;
        a[m] := a[cuoi];
        inc(cuoi);
      end;
    if cuoi-dau>=k then
      kq := (kq - tinh + base + base) mod base;
    dau := cuoi;
  end;
end;
procedure print;
begin
  assign(output,fo);rewrite(output);
  writeln(kq);
  close(output);
end;
begin
  enter;
  process;
  print;
end.

Cấu trúc dữ liệu BIT – Binary Indexed Tree (Fenwick Tree)

Giới thiệu về cây Fenwick (hay còn được gọi là BIT)
BIT-yeulaptrinh.pw

Tổng quát, đặt m = 2k.p (với p là số lẻ). Hay nói cách khác, k là vị trí của bít 1 bên phải nhất của m. Trong Fenwick-Tree, nút có số hiệu m sẽ là nút gốc của một cây con gồm 2k nút có số hiệu từ m- 2k+1 đến m.

Ví dụ:

    – 8 = 23.1, vậy 8 là nút gốc của các nút 1, 2, 3, …, 8.

    – 12 = 22.3, vậy 12 là nút gốc của các nút  9, 10, 11, 12

    – 10 = 21.5, vậy 10 là nút gốc của các nút  9, 10.

    – 7 = 20.7, vậy 7 là nút gốc của chỉ nút  7.

    – 16 = 24.1, vậy 16 là nút gốc của các nút 1, 2, 3, …, 16.

Trong Fenwick-Tree, nút gốc đại diện cho tất cả các nút con của nó. Ý nghĩa của từ đại diện ở đây thường dùng là nút gốc lưu tổng giá trị của các nút con. Vì vậy khi tính toán, ta chỉ cần truy xuất nút gốc là đủ mà không cần thiết phải truy xuất đến các nút con. Xét ví dụ:

       Cho mảng gồm n phần tử a1, a2, …, an. Hãy tính tổng Am =  a1 + a2 + … + am (m ≤ n).

Thay vì sử dụng vòng lặp từ 1 đến m để truy xuất từng phần tử ai một (độ phức tạp O(m)), ta sử dụng cấu trúc FENWICK-TREE như sau:

    – t1 = a1

    – t2 = a1 + a2

    – t3 = a3

    – t4 = a1 + a2 + a3 + a4

    – t5 = a5

    – t6 = a5 + a6

    – t7 = a7

    – t8 = a1 + a2 + a3 + a4+ a5 + a6 + a7 + a8

    – …

    – t12 = a9 + a10 + a11 + a12

    – … (tiếp tục như vậy theo cách xây dựng Fenwick-Tree)

 

    * Để tính A15 (m=15), thay vì phải duyệt từ a1 đến a15, ta chỉ cần tính t8 + t12 + t14 + t15 .

    * Để tính A10, chỉ cần tính t8 + t10

    * Để tính A13, chỉ cần tính t8 + t12 + t13

    * Để tính A16, lấy ngay giá trị t16

Tổng quát với m bất kỳ, biểu diễn m thành dạng nhị phân, sau đó lần lượt xóa các bít 1 của m theo thứ tự từ phải sang trái, tại mỗi bước trung gian chính là chỉ số nút cần truy xuất trong Fenwick-Tree.

Ví dụ: m = 13 có biểu diễn nhị phân là 1101:

     1) 1101 -> truy xuất nút 13

     2) Xóa bít 1 bên phải nhất còn 1100 -> truy xuất nút 12

     3) Xóa bít 1 bên phải nhất còn 1000 -> truy xuất nút 8

     4) Xóa bít 1 bên phải nhất và dừng.

Thao tác truy xuất các nút như trên được gọi là getFENWICK-TREE. May mắn là ta có một công thức rất đơn giản để xóa bít 1 bên phải dùng phép toán AND. Thủ tục getFENWICK-TREE như sau:

int getFENWICK-TREE(int m)
{
            int result = 0;
            for(; m> 0; m &= m-1
            {
                        result += t[m];
            }
            return result;
}

Độ phức tạp của getFENWICK-TREE là O(log2m)

Vấn đề còn lại là làm thế nào để xây dựng được Fenwick-Tree như trên? Cách thực hiện là ban đầu khởi tạo các nút của Fenwick-Tree là 0. Sau đó ứng với mỗi giá trị am thì cập nhật các nút cha liên quan trong cây. Ví dụ:

    – Cập nhật giá trị a5 -> cần cập nhật các nút t5, nút cha t6, nút cha t8, nút cha t16,….

    – Cập nhật giá trị a9 -> cần cập nhật các nút t9, nút cha t10, nút cha t12, nút cha t16,…

    – Cập nhật giá trị a4 -> cần cập nhật các nút t4, nút cha t8, nút cha t16,…

Tổng quát với m bất kỳ, biểu diễn m thành dạng nhị phân, nếu cộng 1 vào bít bên phải nhất của m thì ta được nút cha của m.

Ví dụ, m = 5 có biểu diễn nhị phân là 101:

    1) 101 -> cập nhật nút 5

    2) Cộng 1 vào bít phải nhất thành 0110 -> cập nhật nút 6

    3) Cộng 1 vào bít phải nhất thành 1000 -> cập nhật nút 8

    4) Cộng 1 vào bít phải nhất thành 10000 -> cập nhật nút 16.

Thao tác cập nhật các nút từ con đến cha như trên được gọi là updateFENWICK-TREE. Ta cũng có một công thức rất đơn giản để cộng 1 vào bít 1 bên phải nhất dùng phép toán AND. Thủ tục updateFENWICK-TREE như sau:

void updateFENWICK-TREE(int m, int value)
{
   for(; m<= n; m += m & -m)
   {
        t[m] += value;
   }
}

Độ phức tạp của updateFENWICK-TREE là O(log2n)

Trên đây là lý thuyết về Binary Indexed Tree. Bây giờ ta sẽ áp dụng FENWICK-TREE để giải bài Dãy nghịch thế và Dĩa nhạc 3.

Dãy nghịch thế: (Theo cách vét cạn thì cần xét tất cả các cặp, độ phức tạp là O(n2))

Phác thảo thuật toán:

– Dùng một mảng đếm t[100.000], t[u] cho biết hiện giờ có bao nhiêu số nhỏ hơn u.

– Đầu tiên khởi tạo các phần tử mảng t là 0.

– Duyệt từ cuối mảng lên đầu mảng (i từ n->1), ứng với mỗi ai thực hiện hai thao tác:

    1) Kiểm tra xem hiện giờ có bao nhiêu số nhỏ hơn ai (truy xuất t[ai]).

    2) Cập nhật ai vào mảng t, nghĩa là tăng các phần tử từ t[ai+1] đến t[100.000], mỗi phần tử thêm 1.

Tuy nhiên trong thao tác 2 việc cập nhật như vậy tổng thể độ phức tạp vẫn là O(n2). Bây giờ ta sẽ chuyển mảng t thành cấu trúc FENWICK-TREE. Đối với thao tác 1 dùng getFENWICK-TREE, đối với thao tác 2 dùng updateFENWICK-TREE.

Chương trình hoàn chỉnh

cin>>n;
for(i= 1; i<= n; i++) cin>>a[i];
kq = 0;
for(i= n; i>= 1; i–)
{
            kq += getFENWICK-TREE(a[i]);
            updateFENWICK-TREE(a[i]+1, 1);
}
cout<<kq;

Độ phức tạp là O(nlog2n)

Thông tin về Google Code Jam World Finals 2017

 

Theo dõi link bên dưới:

 

  1. Live Stream

  2. Problemset

  3. Score Board

MSE07B – spoj

Đề bài:

Thuật toán:

  • Nếu bạn sử dụng C++ thì có thể dùng cấu trúc dữ liệu có sẵn là std::set, còn nếu dùng Pascal thì sẽ phải code dài hơn một chút.
  • Toàn bộ về std::set cho bạn nào dùng C++: http://yeulaptrinh.de/938/set-trong-c/

Code:

#include 
using namespace std;
#define FOR(i,a,b) for (int i=(a),_b=(b);i<=_b;i=i+1)
#define FORD(i,b,a) for (int i=(b),_a=(a);i>=_a;i=i-1)
#define REP(i,n) for (int i=0,_n=(n);i<_n;i=i+1)
#define FORE(i,v) for (__typeof((v).begin()) i=(v).begin();i!=(v).end();i++)
#define ALL(v) (v).begin(),(v).end()
#define pb push_back
#define mp make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
#define double db
typedef long long ll;
typedef pair PII;
const ll mod=1000000007;
ll powmod(ll a,ll b) {ll res=1;a%=mod; assert(b>=0); for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}
const int MAXN = 1E6+3;
const int oo = 1e9+3;

set s;
set::iterator it;
int p, k, x, i;

int main() {
    	#ifndef ONLINE_JUDGE
    	freopen("test.inp", "r", stdin);
    	freopen("test.out", "w", stdout);
    	#endif
    cin >> x;
    while (x != 0) {
        //
        if (x == 1) {
            cin >> k >> p;
            s.insert(PII(p,k));
        }
        //
        if (x == 2)
        if (!s.empty()) {
            it = s.end(); -- it;
            cout << it -> se << endl;
            s.erase(it);
        } else cout << 0 << endl;
        //
        if (x==3)
        if (!s.empty()) {
            it = s.begin();
            cout << it -> se << endl;
            s.erase(it);
        } else cout << 0 << endl;
        //
        cin >> x;
    }
	return 0;
}

Set trong C++

Tổng quan

  • Set là một loại associative containers để lưu trữ các phần tử không bị trùng lặp (unique elements), và các phần tử này chính là các khóa (keys).
  • Khi duyệt set theo iterator từ begin đến end, các phần tử của set sẽ tăng dần theo phép toán so sánh.
  • Mặc định của set là sử dụng phép toán less, bạn cũng có thể viết lại hàm so sánh theo ý mình.
  • Set được thực hiện giống như cây tìm kiếm nhị phân (Binary search tree).

Khai báo:

#include 
set  s;
set  > s;

Hoặc viết class so sánh theo ý mình:

struct cmp{
bool operator() (int a,int b) {return a myset ;

Capacity:

  • size : trả về kích thước hiện tại của set. ĐPT O(1)
  • empty : true nếu set rỗng, và ngược lại. ĐPT O(1).

Thay đổi:

  • insert : Chèn phần tử vào set. ĐPT O(logN).
  • erase : có 2 kiểu xóa: xóa theo iterator, hoặc là xóa theo khóa. ĐPT O(logN).
  • clear : xóa tất cả set. ĐPT O(n).
  • swap : đổi 2 set cho nhau. ĐPT O(n).

Truy cập phần tử:

  • find : trả về itarator trỏ đến phần tử cần tìm kiếm. Nếu không tìm thấy itarator trỏ về “end” của set. ĐPT O(logN).
  • lower_bound : trả về iterator đến vị trí phần tử bé nhất mà không bé hơn (lớn hơn hoặc bằng) khóa (dĩ nhiên là theo phép so sánh), nếu không tìm thấy trả về vị trí “end” của set. ĐPT O(logN).
  • upper_bound: trả về iterator đến vị trí phần tử bé nhất mà lớn hơn khóa, nếu không tìm thấy trả về vị trí “end” của set.. ĐPT O(logN).
  • count : trả về số lần xuất hiện của khóa trong container. Nhưng trong set, các phần tử chỉ xuất hiện một lần, nên hàm này có ý nghĩa là sẽ return 1 nếu khóa có trong container, và 0 nếu không có. ĐPT O(logN).

 

Chương trình Demo 1:

#include 
#include 
using namespace std;
main() {
      set  s;
      set  ::iterator it; s.insert(9);	// s={9}
      s.insert(5);	// s={5,9} cout << *s.begin() << endl; //In ra 5 s.insert(1);	// s={1,5,9} cout << *s.begin() << endl; // In ra 1

      it=s.find(5);
      if (it==s.end()) cout << "Khong co trong container" << endl; else  cout << "Co trong container" << endl;

      s.erase(it);	// s={1,9}
      s.erase(1);	// s={9}

      s.insert(3);	// s={3,9}
      s.insert(4);	// s={3,4,9}

      it=s.lower_bound(4);
      if (it==s.end()) cout << "Khong co phan tu nao trong set khong be hon 4" << endl; else cout << "Phan tu be nhat khong be hon 4 la " << *it << endl;  // In ra 4

      it=s.lower_bound(10);
      if (it==s.end()) cout << "Khong co phan tu nao trong set khong be hon 10" << endl; else cout << "Phan tu be nhat khong be hon 10 la " << *it << endl; // Khong co ptu nao

      it=s.upper_bound(4);
      if (it==s.end()) cout << "Khong co phan tu nao trong set lon hon 4" << endl; else cout << "Phan tu be nhat lon hon 4 la " << *it << endl;	// In ra 9

      /* Duyet set */

      for (it=s.begin();it!=s.end();it++) { cout << *it <<  " ";
      }
      // In ra 3 4 9

      cout << endl; system("pause");
}

Lưu ý: Nếu bạn muốn sử dụng hàm lower_bound hay upper_bound để tìm phần tử lớn nhất “bé hơn hoặc bằng” hoặc “bé hơn” bạn có thể thay đổi cách so sánh của set để tìm kiếm. Mời bạn xem chương trình sau để rõ hơn:

#include 
#include 
#include  using namespace std;
main() {
      set  > s;
      set  > :: iterator it; // Phép toán so sánh là greater

      s.insert(1);	// s={1}
      s.insert(2);	// s={2,1}
      s.insert(4);	// s={4,2,1}
      s.insert(9);	// s={9,4,2,1}

      /* Tim phần tử lớn nhất bé hơn hoặc bằng 5 */ it=s.lower_bound(5);
      cout << *it << endl;	// In ra 4

      /* Tim phần tử lớn nhất bé hơn 4 */ it=s.upper_bound(4);
      cout << *it << endl;	// In ra 2

      system("pause");
}

Vector trong C++

Khai báo vector:

Vector có thể hiểu là một mảng có trình tự, giống như với danh sách liên kết hay một chuỗi thông thường nhưng “vector” khác với chuỗi hoăc mảng thông thường là chúng ta có thể thay đổi kích thước của nó và cũng có thể truy cập trực tiếp đến các phần tử, điều này làm cho việc sử dụng “vector” linh hoạt hơn so với “list”…

#include 
...
/* Vector 1 chiều */

/* tạo vector rỗng kiểu dữ liệu int */ vector  first;

//tạo vector với 4 phần tử là 100 vector  second (4,100);

// lấy từ đầu đến cuối vector second
vector  third (second.begin(),second.end())

//copy từ vector third vector  four (third)

/*Vector 2 chiều*/

/* Tạo vector 2 chiều rỗng */ vector < vector  > v;

/* khai báo vector 5×10 */
vector < vector  > v (5, 10) ;

/* khai báo 5 vector 1 chiều rỗng	*/ vector < vector  > v (5) ;

//khai báo vector 5*10 với các phần tử khởi tạo giá trị là 1 vector < vector  > v (5, vector  (10,1) ) ;

Các bạn chú ý 2 dấu “ngoặc” không được viết liền nhau. Ví dụ như sau là sai:

/*Khai báo vector 2 chiều SAI*/
vector > v;

Các hàm thành viên:

  • size : trả về số lượng phần tử của vector. ĐPT O(1).
  • empty : trả về true(1) nếu vector rỗng, ngược lại là false(0). ĐPT O(1).

 

Truy cập tới phần tử:

  • operator [] : trả về giá trị phần tử thứ []. ĐPT O(1).
  • at : tương tự như trên. ĐPT O(1).
  • front: trả về giá trị phần tử đầu tiên. ĐPT O(1).
  • back: trả về giá trị phần tử cuối cùng. ĐPT O(1).

 

Chỉnh sửa:

  • push_back : thêm vào ở cuối vector. ĐPT O(1).
  • pop_back : loại bỏ phần tử ở cuối vector. ĐPT O(1).
  • insert (iterator,x): chèn “x” vào trước vị trí “iterator” ( x có thể là phần tử hay iterator của 1 đoạn phần tử…). ĐPT O(n).
  • erase : xóa phần tử ở vị trí iterator. ĐPT O(n).
  • swap : đổi 2 vector cho nhau (ví dụ: first.swap(second);). ĐPT O(1).
  • clear: xóa vector. ĐPT O(n).

 

Nhận xét:

  • Sử dụng vector sẽ tốt khi:
    • Truy cập đến phần tử riêng lẻ thông qua vị trí của nó O(1)
    • Chèn hay xóa ở vị trí cuối cùng O(1).
  • Vector làm việc giống như một “mảng động”.

 

Một vài ví dụ:

Ví dụ 1: Ví dụ này chủ yếu để làm quen sử dụng các hàm chứ không có đề bài cụ thể.

#include 
#include 
using namespace std;
vector  v; //Khai báo vector
vector ::iterator it;	//Khai báo iterator
vector ::reverse_iterator rit; //Khai báo iterator ngược int i;
main() {
      for (i=1;i<=5;i++) v.push_back(i); // v={1,2,3,4,5} cout << v.front() << endl;	// In ra 1
      cout << v.back() << endl;	// In ra 5

      cout << v.size() << endl;	// In ra 5

      v.push_back(9);	// v={1,2,3,4,5,9}
      cout << v.size() << endl;	// In ra 6

      v.clear();	// v={}
      cout << v.empty() << endl;	// In ra 1 (vector rỗng)

      for (i=1;i<=5;i++) v.push_back(i); // v={1,2,3,4,5} v.pop_back();	// v={1,2,3,4}
      cout << v.size() << endl;	// In ra 4

      v.erase(v.begin()+1);	// Xóa ptử thứ 1 v={1,3,4} v.erase(v.begin(),v.begin()+2);	// v={4}
      v.insert(v.begin(),100);	// v={100,4}
      v.insert(v.end(),5);	// v={100,4,5}

      /*Duyệt theo chỉ số phần tử*/
      for (i=0;i

Ví dụ 2: Cho đồ thị vô hướng G có n đỉnh (các đỉnh đánh số từ 1 đến n) và m cạnh và không có khuyên (đường đi từ 1 đỉnh tới chính đỉnh đó).

Cài đặt đồ thị bằng danh sách kề và in ra các cạnh kề đối với mỗi cạnh của đồ thị. Dữ liệu vào:

  • Dòng đầu chứa n và m cách nhau bởi dấu cách
  • M dòng sau, mỗi dòng chứa u và v cho biết có đường đi từ u tới Không có cặp đỉnh u,v nào chỉ cùng 1 đường đi.

Dữ liệu ra:

  • M dòng: Dòng thứ i chứa các đỉnh kề cạnh i theo thứ tự tăng dần và cách nhau bởi dấu cách.

Giới hạn: 1 <= n,m <= 10000 Ví dụ:

INPUT OUTPUT
6 7 2 3 5 6
1 2 1 3 6
1 3 1 2 5
1 5  
2 3 1 3
2 6 1 2
3 5  
6 1  

 

Chương trình mẫu:

#include 
#include  using namespace std;
vector < vector  > a (10001);

//Khai báo vector 2 chiều với 10001 vector 1 chiều rỗng int m,n,i,j,u,v;
main() {
      /*Input data*/ cin >> n >> m;
      for (i=1;i<=m;i++) { cin >> u >> v; a[u].push_back(v);
      a[v].push_back(u);
      }
      /*Sort cạnh kề*/ for (i=1;i<=m;i++)
      sort(a[i].begin(),a[i].end());
      /*Print Result*/
      for (i=1;i<=m;i++) {
      for (j=0;j

Một số kỹ thuật tối ưu hóa thuật toán quy hoạch động

 

LỜI NÓI ĐẦU

 

Quy hoạch động (QHĐ) là một lớp thuật toán rất quan trọng và có nhiều ứng dụng trong ngành khoa học máy tính. Trong các cuộc thi Olympic tin học hiện đại, QHĐ luôn là một trong những chủ đề chính. Tuy vậy, theo tôi thấy, tài liệu nâng cao về QHĐ bằng tiếng Việt hiện còn cực kỳ khan hiếm, dẫn đến học sinh/sinh viên Việt Nam bị hạn chế khả năng tiếp cận với những kỹ thuật hiện đại. Trong bài viết này, tôi sẽ trình bày một vài kỹ thuật để tối ưu hóa độ phức tạp của một số thuật toán QHĐ.

 

Lê Anh Đức A2K42-PBC

  1. Đổi biến

Nhiều khi trong trạng thái QHĐ có một thành phần nào đấy với khoảng giá trị quá lớn, trong khi kết quả của hàm lại có khoảng giá trị nhỏ. Trong một vài trường hợp, ta có thể đảo nhãn để giảm số trạng thái.

 

Bài tập ví dụ

 

Longest Common Subsequence (bài toán cổ điển LCS)

(bộ test đi kèm)

 

Cho xâu A độ dài m, xâu B độ dài n. Hãy tìm độ dài xâu con chung dài nhất của hai xâu, chú ý là xâu con chung có thể không liên tiếp.

 

Giới hạn:

 

  • m <= 1 000 000
  • n <= 5 000
  • Các kí tự trong cả hai xâu là các chữ cái tiếng Anh in hoa ‘A’..’Z’

 

Input:

 

  • Dòng thứ nhất chứa xâu A
  • Dòng thứ hai chứa xâu B

 

Output:

 

  • Dòng duy nhất chứa kết quả

 

 

Ví dụ:

Input Output
ADBCC

ABCD

3

 

Lời giải

Thuật toán đơn giản

Gọi F(i, j) là LCS của hai tiền tố A[1..i] và B[1..j].

Khi đó ta có thể maximize F(i, j) theo F(i-1, j) và F(i, j-1).

Nếu A[i] = B[j] thì ta có thể cập nhật F(i, j) theo F(i-1, j-1) + 1.

Kết quả bài toán là F(m, n).

Độ phức tạp của thuật toán này là O(m*n), không khả thi với giới hạn của đề bài.

 

Đổi biến

Đặt L = min(m, n);

Để ý rằng trong hàm QHĐ trên, các giá trị của F(i, j) sẽ không vượt quá L, trong khi đó chiều thứ hai của trạng thái có thể khá lớn (lên tới MAXM = 1 000 000).

Để tối ưu hóa, ta sẽ đổi biến. Gọi dp(i, j) là vị trí k nhỏ nhất sao cho LCS(A[1..i], B[1..k]) = j.

Để tính các giá trị của dp(), ta sẽ QHĐ theo kiểu cập nhật đi, thay vì đi tìm công thức trực tiếp cho các dp(i, j).

Gọi nextPos(i, c) = j > i nhỏ nhất mà a[j] = c (với c là một ký tự từ ‘A’ đến ‘Z’).

Mảng nextPos[][] có thể tính trong O(M*26).

Như vậy ta có thể tính các giá trị QHĐ như sau:

 

  • Ban đầu khởi tạo các giá trị dp(i, j) = vô cùng, dp(0, 0) = 0.
  • For i và j tăng dần, với mỗi giá trị dp(i, j) khác vô cùng:

 

    • Cập nhật dp(i+1, j) theo dp(i, j).
    • Gọi k là vị trí xuất hiện tiếp theo của b[i+1] trong xâu A bắt đầu từ vị trí dp(i, j), tức là k = nextPos(dp(i, j), b[i+1]).
    • Nếu tồn tại k, cập nhật dp(i+1, j+1) theo k.

Để tính kết quả, ta sẽ chỉ cần tìm j lớn nhất mà tồn tại dp(i, j) khác vô cùng.

 

#include <bits/stdc++.h>

 

using namespace std;

 

const int M = 1e6 + 6;

const int N = 5005;

 

int dp[N][N];

 

char a[M], b[N];

int nextPos[M][26];

int m, n;

 

void minimize(int &a, int b) {

if (a == -1 || a > b) a = b;

}

 

int main() {

cin >> a + 1 >> b + 1;

m = strlen(a + 1); n = strlen(b + 1);

for (int c = 0; c < 26; ++c)

for (int i = m – 1; i >= 0; –i)

nextPos[i][c] = (a[i + 1] – ‘A’ == c) ? i + 1 :                 nextPos[i + 1][c];

int maxLength = min(m, n);

memset(dp, -1, sizeof dp);

dp[0][0] = 0;

for (int i = 0; i < n; ++i) {

for (int j = 0; j <= i; ++j) if (dp[i][j] >= 0) {

minimize(dp[i + 1][j], dp[i][j]);

int new_value = nextPos[dp[i][j]][b[i + 1] – ‘A’];

if (new_value > 0)

minimize(dp[i + 1][j + 1], new_value);

}

}

int ans = 0;

for (int j = maxLength; j > 0; –j) {

for (int i = j; i <= n; ++i)

if (dp[i][j] >= 0) ans = j;

if (ans != 0) break;

}

cout << ans << endl;

return 0;

}

 

Bài tập ví dụ

COMPUTER (VNOI Marathon 2010)

Submit: vn.spoj.com/problems/COMPUTER/

 

Công ty phần mềm XYZ mới mua x máy tính để bàn và y máy tính xách tay. Giá một máy tính để bàn là a đồng còn giá một máy tính xách tay là b đồng. Để tránh sự thắc mắc giữa các phòng bàn, Tổng giám đốc đã đưa ra cách phân bố các máy tính này về n phòng ban như sau:

 

  • Sắp xếp n phòng ban theo thứ tự về mức độ quan trọng của các phòng ban.
  • Tiến hành phân bố các máy tính cho các phòng ban bảo đẩm nếu phòng ban i có mức độ quan trọng nhỏ hơn mức độ quan trọng của phòng ban j thì tổng giá trị máy tính được phân bố cho phòng ban i không được vượt quá tổng giá trị máy tính được phân bố cho phòng ban j.
  • Phòng ban nhận được tổng giá trị máy tính nhỏ nhất là lớn nhất.

 

Là một lập trình viên giỏi nhưng lại thuộc phòng ban có mức độ quan trọng nhỏ nhất, Thắng muốn chứng tỏ tay nghề của mình với đồng nghiệp nên đã lập trình tính ra ngay được tổng giá trị máy trình mà phòng ban mình nhận được rồi mời bạn tính lại thử xem!

 

Yêu cầu:

Cho x, a, y, b, n. Hãy tính tổng giá trị máy tính mà phòng Thắng nhận được.

 

Input:

Gồm hai bộ dữ liệu, mỗi bộ trên một dòng, mỗi dòng chứa 5 số nguyên dương x, a, y, b, n (các số có giá trị không vượt quá 1000).

Output:

Gồm hai dòng, mỗi dòng là đáp án tương ứng với bộ dữ liệu vào.

 

Ví dụ:

Input Output
3 300 2 500 2

4 300 3 500 2

900

1300

 

Lời giải

Trước hết ta sẽ chặt nhị phân kết quả bài toán. Với mỗi giá trị chặt nhị phân, ta cần kiểm tra xem có tồn tại phương án thỏa mãn hay không.

 

Thuật toán sơ khai

Đặt giá trị cần kiểm tra là v.

Xét các phòng ban theo thứ tự tăng dần về mức độ quan trọng, đánh số từ 1.

Sử dụng một mảng đa chiều để đánh dấu các trạng thái có thể đạt tới. Các giá trị cần quản lí là: chỉ số của phòng ban, đã dùng số máy tính để bàn x, đã dùng số máy tính xách tay y, tổng giá trị máy tính của phòng ban trước đó.

Bắt đầu từ trạng thái (0, 0, 0, 0), ta sử dụng thuật toán loang (BFS). Cuối cùng nếu trạng thái (n, 0, 0, …) có thể đến được, thì ta sẽ có cách phân hoạch các máy tính vào các phòng ban ứng với giá trị cận dưới v.

Không cần tính toán cụ thể cũng có thể thấy thuật toán này không thể đáp ứng về mặt thời gian (và bộ nhớ) với giới hạn của đề bài.

 

Nâng cấp bằng nhận xét

Nhận xét rằng ta không cần quan tâm tới thứ tự về mức độ quan trọng của các phòng ban. Với một cách phân hoạch các máy tính sao cho mỗi phòng nhận được tổng giá trị không nhỏ hơn v, ta luôn có thể sắp xếp các bộ theo giá trị không giảm ứng với các phòng ban.

Ta có trạng thái QHĐ là F(i, x, y, value) = true nếu có thể phân bổ máy tính cho i phòng ban, đã dùng x máy tính để bàn và y máy tính xách tay, đã gom được tổng giá trị v cho phòng thứ i+1. Cách làm này số trạng thái vẫn như trước nhưng ta đã có thể chuyển trạng thái trong O(1). Cụ thể từ F(i, x, y, value) ta chuyển đến F(i, x+1, y, value+a) hoặc F(i, x, y+1, value+b), chú ý là chỉ có thể dùng thêm máy xách tay nếu x<X và dùng thêm máy để bàn nếu y<Y, đồng thời nếu giá trị value đủ lớn hơn hoặc bằng v thì ta chuyển sang trạng thái F(i+1, x, y, 0) luôn.

 

Đổi biến

Ở bài này, ta có thể dễ dàng đổi biến value ra làm hàm mục tiêu. Nhưng không chỉ có vậy, ta có thể đẩy cả i ra ngoài! Cụ thể, F(x, y) = một cặp số (i, value) lần lượt là số phòng phân bố được và số tiền gom được. Hàm mục tiêu của F(x, y) là một cặp số hoàn toàn có thể so sánh được, trong đó giá trị đầu (i) được ưu tiên so sánh trước.

Cách cập nhật các F(x, y) giống như phần trước, độ phức tạp vẫn là O(1) cho bước chuyển trạng thái, trong khi số trạng thái lúc này là đủ nhỏ đối với giới hạn của đề bài.

 

#include <bits/stdc++.h>

 

using namespace std;

 

const int N = 1010;

 

int x, y, a, b, n;

 

pair<int, int> F[N][N];

 

pair<int, int> newState(pair<int, int> s, int a, int v) {

s.second += a;

if (s.second >= v) {

++s.first;

s.second = 0;

}

return s;

}

 

bool dp(int value) {

for (int i = 0; i <= x; ++i) for (int j = 0; j <= y; ++j)

F[i][j] = make_pair(0, 0);

for (int i = 0; i <= x; ++i) for (int j = 0; j <= y; ++j) {

if (F[i][j].first == n) return 1;

if (i < x)

F[i + 1][j] = max(F[i + 1][j], newState(F[i][j], a, value));

if (j < y)

F[i][j + 1] = max(F[i][j + 1], newState(F[i][j], b, value));

}

return 0;

}

 

int solve() {

int l = 0, r = (a * x + b * y) / n;

int ans = 0;

while (l <= r) {

int mid = l + r >> 1;

if (dp(mid)) {

ans = mid;

l = mid + 1;

} else {

r = mid 1;

}

}

return ans;

}

 

int main() {

cin >> x >> a >> y >> b >> n;

cout << solve() << endl;

cin >> x >> a >> y >> b >> n;

cout << solve() << endl;

return 0;

}

 

Bài luyện tập: http://vn.spoj.com/problems/BINPACK/

 

2. Chia để trị

Đây là kỹ thuật khá hiếm gặp, tuy nhiên lại cực kỳ mạnh.

 

Bài tập ví dụ

Hai nhà máy (CEOI 2004)

 

Có n cây cổ thụ được trồng trên một con đường từ đỉnh đổi đến chân đồi. Chính phủ địa phương quyết định cắt bỏ chúng. Để tránh hoang phí, mỗi cái cây cần được chuyển đến một nhà máy cưa.

 

Cây chỉ có thể được vận chuyển theo một chiều duy nhất: hướng về chân đồi. Có một nhà máy cưa ở cuối con đường. Hai nhà máy cưa có thể được xây dựng dọc theo con đường. Hãy xác định vị trí tối ưu để xây dựng chúng, để cực tiểu hóa chi phí vận chuyển. Chi phí vận chuyển 1kg gỗ đi 1 mét là 1 cent.

 

Yêu cầu

Viết chương trình:

 

  • đọc dữ liệu từ đầu vào chuẩn số lượng cây, khối lượng và vị trí của chúng,
  • tính toán chi phí vận chuyển tối ưu nhất,
  • xuất kết quả ra đầu ra chuẩn.

 

Input

Dòng đầu tiên chứa số n – số lượng cây (2 <= n <= 20000). Các cây được đánh số 1, 2, …, n, theo chiều từ đỉnh đồi đến chân đồi.

n dòng tiếp theo mỗi dòng chứa hai số nguyên dương cách nhau bởi dấu cách. Dòng thứ i + 1 chứa w(i) – khối lượng tính theo kg của cái cây thử i và d(i) – khoảng cách tính theo mét giữa cây thứ i và cây i + 1, 1 <= w(i) <= 10000, 0 <= d(i) <= 10000. Số cuối cùng, d(n) là khoảng cách từ cây thứ n đến chân đồi.

Dữ liệu vào đảm bảo kết quả của bài toán không vượt quá 2 000 000 000 cent.

 

Output

Một dòng duy nhất chứa một số là kết quả bài toán: chi phí vận chuyển nhỏ nhất.

 

Ví dụ

Input Output
9

1 2

2 1

3 3

1 1

3 2

1 6

2 1

1 2

1 1

26

Hình vẽ trên minh họa cho test ví dụ. Các hình tròn được tô đen là các vị trí có nhà máy. Kết quả sẽ là:
1 * (2+1) + 2 * 1 + 1 * (1 + 2) + 3 * 2 + 2 * (1 + 2 + 1) + 1 * (2 + 1) + 1 * 1 = 26.

Lời giải

 

Trước hết ta sẽ giải quyết vấn đề tính chi phí vận chuyển nếu biết vị trí của hai nhà máy đặt thêm.

Nếu ta có thể tính được chi phí này trong O(1), bài toán sẽ có thể giải được trong O(N^2) – ta có thể for hết các cặp vị trí có thể đặt nhà máy.

Gọi:

 

  • sumW(i) = tổng của các w(j) với i <= j.
  • sumD(i) = tổng của các d(j) với i <= j.
  • sumWS(i) = tổng của các w(j) * sumD(j) với i <= j.

 

Khi đó cost(L, R) = chi phí vận chuyển các cây có chỉ số trong đoạn [L..R] đến nhà máy đặt ở R là: sumWS(L) – sumWS(R) – sumD(R) * (sumW(L) – sumW(R)).

Như vậy ta có thể xây dựng hàm eval(i, j) = chi phí nếu đặt thêm hai nhà máy ở i và j = cost(1, i) + cost(i + 1, j) + cost(j + 1, n + 1).

 

Tuy nhiên lời giải O(N^2) là chưa đủ tốt để có thể giải quyết trọn vẹn bài toán này.

Gọi best(i) = vị trí j > i tốt nhất nếu ta đã đặt một nhà máy ở i.

Như vậy kết quả của bài toán sẽ là min(eval(i, best(i)) với 1 <= i < n.

Nhận xét:

 

  • best(i) <= best(i + 1).
  • Ta có thể tính các best(i) theo thứ tự bất kỳ.

 

Như vậy ta có thuật toán sử dụng tư tưởng chia để trị như sau:

Hàm solve(L, R, from, to) sẽ đi tính các best(L..R), biết rằng chúng nằm trong đoạn [from..to].

 

void solve(int L, int R, int from, int to) {

if (L > R) return;

int mid = L + R >> 1;

best[mid] = from;

for (int i = from + 1; i <= to; ++i)

if (eval(mid + 1, best[mid]) > eval(mid + 1, i))

best[mid] = i;

solve(L, mid – 1, from, best[mid]);

solve(mid + 1, R, best[mid], to);

}

Đánh giá độ phức tạp thuật toán: vì mỗi lần gọi để quy khoảng L..R được chia đôi, nên sẽ có O(logN) tầng, mỗi tầng vòng for chỉ chạy qua O(N) phần tử, vì vậy độ phức tạp của thuật toán là O(NlogN).

 

Bài tập ví dụ

SEQPART

Submit: hackerrank.com

 

Cho dãy L số C[1..L], cần chia dãy này thành G đoạn liên tiếp. Với phần tử thứ i, ta định nghĩa chi phí của nó là tích của C[i] và số lượng số nằm cùng đoạn liên tiếp với nó. Chi phí của dãy số ứng với một cách phân hoạch là tổng các chi phí của các phần tử.

Hãy xác định cách phân hoạch dãy số để chi phí là nhỏ nhất.

 

Input:

 

  • Dòng đầu tiên chứa 2 số L và G.
  • L dòng tiếp theo, chứa giá trị của dãy C.

 

Output:

 

  • Một dòng duy nhất chứa chi phí nhỏ nhất.

 

Giới hạn:

 

  • 1 <= L <= 8000
  • 1 <= G <= 800
  • 1 <= C[i] <= 1 000 000 000

 

Ví dụ:

Input Output
6 3

11

11

11

24

26

100

299

 

Giải thích: cách tối ưu là C[] = (11, 11, 11), (24, 26), (100). Chi phí là 11 * 3 + 11 * 3 + 11 * 3 + 24 * 2 + 26 * 2 + 100 * 1 = 299.

 

Lời giải

Đây là dạng bài toán phân hoạch dãy số có thể dễ dàng giải bài QHĐ. Gọi F(g, i) là chi phí nhỏ nhất nếu ta phân hoạch i phần tử đầu tiên thành g nhóm, khi đó kết quả bài toán sẽ là F(G, L).

Để tìm công thức truy hồi cho hàm F(g, i), ta sẽ quan tâm đến nhóm cuối cùng. Coi phần tử 0 là phần tử cầm canh ở trước phần tử thứ nhất, thì người cuối cùng không thuộc nhóm cuối có chỉ số trong đoạn [0..i]. Giả sử đó là người với chỉ số k, thì chi phí của cách phân hoạch sẽ là F(g-1, k) + Cost(k+1, i), với Cost(i, j) là chi phí nếu phân j-i+1 người có chỉ số [i..j] vào một nhóm. Như vậy:

F(g, i) = min[F(g-1, k) + Cost(k+1, l)] với 0 <= k <= i.

Chú ý là công thức này chỉ được áp dụng với g>1, nếu g=1, F(1, i) = Cost(1, i), đây là trường hợp cơ sở.

Việc cài đặt chỉ đơn giản là dựng mảng 2 chiều F[][], code như sau:

 

#include <iostream>

 

using namespace std;

 

const int MAXL = 8008;

const int MAXG = 808;

const long long INF = (long long)1e18;

 

long long C[MAXL];

long long sum[MAXL];

long long F[MAXG][MAXL];

 

long long cost(int i, int j) {

return (sum[j] – sum[i – 1]) * (j – i + 1);

}

 

int main() {

int G, L;

cin >> L >> G;

for (int i = 1; i <= L; ++i) {

cin >> C[i];

sum[i] = sum[i – 1] + C[i];

}

 

for (int g = 1; g <= G; ++g) {

for (int i = 0; i <= L; ++i) {

if (g == 1) {

F[g][i] = cost(1, i);

} else {

F[g][i] = INF;

for (int k = 0; k <= i; ++k) {

long long new_cost = F[g – 1][k] + cost(k + 1, i);

if (F[g][i] > new_cost) F[g][i] = new_cost;

}

}

}

}

cout << F[G][L] << endl;

return 0;

}

Chú ý là ta sử dụng mảng sum[] tiền xử lí O(L) để có thể truy vấn tổng một đoạn (dùng ở hàm cost()) trong O(1). Như vậy độ phức tạp của thuật toán này là O(G*L*L).

 

Thuật toán tối ưu hơn

Gọi P(g, i) là k nhỏ nhất để cực tiể hóa F(g, i), nói cách khác, P(g, i) là k nhỏ nhất mà F(g, i) = F(g-1, k) + Cost(k+1, i).

Tính chất quan trọng để có thể tối ưu thuật toán trên là dựa vào tính đơn điệu của P(g, i), cụ thể:

P(g, 0) <= P(g, 1) <= P(g, 2) <= … <= P(g, L-1) <= P(g, L)

Ta sẽ không chứng minh điều này ở đây, độc giả có thể tự thuyết phục rằng điều này là đúng.

 

Chia để trị

Để ý rằng để tính F(g, i), ta chỉ cần quan tâm tới hàng trước <F(g-1)> của ma trận:

F(g-1, 0), F(g-1, 1), … , F(g-1, L). Như vậy, ta có thể tính hàng F(g) theo thứ tự bất kỳ.

Ý tưởng là với hàng g, trước hết ta tính F(g, mid) và P(g, mid) với mid=L/2, sau đó sử dụng tính chất nêu trên P(g, i) <= P(g, mid) với i < mid và P(g, i) >= P(g, mid) với i > mid để đi gọi đệ quy đi tính hai nửa còn lại.

 

#include <iostream>

 

const int MAXL = 8008;

const int MAXG = 808;

const long long INF = (long long)1e18;

 

using namespace std;

 

long long F[MAXG][MAXL], sum[MAXL], C[MAXL];

int P[MAXG][MAXL];

 

long long cost(int i, int j) {

if (i > j) return 0;

return (sum[j] – sum[i – 1]) * (j – i + 1);

}

 

void solve(int g, int L, int R, int optL, int optR) {

if (L > R) return;

int mid = (L + R) / 2;

F[g][mid] = INF;

for (int i = optL; i <= optR; ++i) {

long long new_cost = F[g – 1][i] + cost(i + 1, mid);

if (F[g][mid] > new_cost) {

F[g][mid] = new_cost;

P[g][mid] = i;

}

}

solve(g, L, mid – 1, optL, P[g][mid]);

solve(g, mid + 1, R, P[g][mid], optR);

}

 

int main() {

int G, L;

cin >> L >> G;

for (int i = 1; i <= L; ++i) {

cin >> C[i];

sum[i] = sum[i – 1] + C[i];

}

for (int i = 1; i <= L; ++i) F[1][i] = cost(1, i);

for (int g = 2; g <= G; ++g) solve(g, 1, L, 1, L);

cout << F[G][L] << endl;

return 0;

}

Chú ý rằng ta không thể đảm bảo rằng P[g][mid] chia đôi đoạn [optL..optR], thực tế một vài hàm solve() sẽ chạy chậm hơn nhiều hàm solve() khác.

Tuy nhiên ta có thể chứng minh được, xét về tổng thế thuật toán này chạy đủ nhanh. Mỗi lần ta chia đôi đoạn [L..R], nên ta sẽ đảm bảo có tối đa O(log(L)) tầng đệ quy, như vậy với mỗi hàng g, ta chỉ mất O(L logL) để tính. Toàn bộ thuật toán có độ phức tạp là O(GLlogL).

 

Bài luyện tập: https://www.hackerrank.com/contests/world-codesprint-5/challenges/mining

 

Toán tử trong C++

Trong bài ta sẽ cùng tìm hiểu các vấn đề:

  • Toán tử quan hệ trong C++
  • Toán tử logic trong C++
  • Toán tử trên bit trong C++
  • Các toán tử hỗn hợp trong C++
  • Độ ưu tiên toán tử trong C++

Toán tử quan hệ trong C++ (Relational operators)

Toán tử quan hệ dùng để so sánh 2 toán hạng với nhau. Sẽ trả về 2 giá trị là 1 (true) hoặc 0 (false).

Bảng bên dưới mô tả các toán tử quan hệ trong C++, giả sử x = 6, y = 9:

Operator Symbol Form Operation Example
Greater than > x > y true if x is greater than y, false otherwise (x > y) is false
Less than < x < y true if x is less than y, false otherwise (x < y) is true
Greater than or equals >= x >= y true if x is greater than or equal to y, false otherwise (x >= y) is false
Less than or equals <= x <= y true if x is less than or equal to y, false otherwise (x <= y) is true
Equality == x == y true if x equals y, false otherwise (x == y) is false
Inequality != x != y true if x does not equal y, false otherwise (x != y) is true

Chú ý: Phân biệt toán tử gán bằng (=) và toán tử so sánh bằng (==).

Ví dụ:

#include <iostream> using namespace std; int sum(int a, int b) { return a + b; } int main() { cout << "Enter an integer: "; int x; cin >> x; cout << "Enter another integer: "; int y; cin >> y; if (x == y) cout << x << " == " << y << "\n"; if (x != y) cout << x << " != " << y << "\n"; if (x > y) cout << x << " > " << y << "\n"; if (x < y) cout << x << " < " << y << "\n"; if (x >= y) cout << x << " >= " << y << "\n"; if (x <= y) cout << x << " <= " << y << "\n"; return 0; }
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

Outputs:

Operators

 

Toán tử quan hệ và so sánh số chấm động?

Trong lập trình, việc so sánh trực tiếp 2 số chấm động là điều không nên và có thể cho ra những kết quả không mong muốn. Đó là do lỗi làm tròn của số chấm động, vấn đề này đã được giải thích trong bài Số tự nhiên và Số chấm động trong C++ (Integer, Floating point).

Ví dụ:

#include <iostream> #include <iomanip> // for std::setprecision() using namespace std; int main() { double d1{ 1.0 }; double d2{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; if (d1 == d2) cout << "d1 == d2" << "\n"; else if (d1 > d2) cout << "d1 > d2" << "\n"; else if (d1 < d2) cout << "d1 < d2" << "\n"; cout << std::setprecision(20); // show 20 digits cout << "d1 = " << d1 << endl; cout << "d2 = " << d2 << endl; return 0; }
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Outputs:

Operators

 

Trong chương trình trên, trong toán học thì 2 biến d1 == d2, nhưng trong lập trình biến d1 > d2 vì lỗi làm tròn số dấu chấm động.

Tương tự, bạn hãy thử với trường hợp 0.1 + 0.7 == 0.8 ?

Chú ý: Không bao giờ so sánh hai giá trị dấu chấm động bằng nhau hay không. Hầu như luôn luôn có sự khác biệt nhỏ giữa hai số chấm động. Cách phổ biến để so sánh 2 số chấm động là tính khoảng cách giữa 2 số đó, nếu khoảng cách đó là rất nhỏ thì ta cho là bằng nhau. Giá trị dùng để so sánh với khoảng cách đó thường được gọi là epsilon.

Toán tử logic trong C++ (Logical operators)

Nếu chỉ sử dụng toán tử quan hệ để so sánh biểu thức quan hệ đúng hay sai, bạn chỉ có thể kiểm tra một điều kiện tại một thời điểm. Nhưng thực tế, có lúc bạn sẽ cần kiểm tra nhiều điều kiện cùng lúc.

Ví dụ: để trở thành một soái ca thì bạn phải có nhiều điều kiện như cầm, kỳ, thi, họa. Lúc này không chỉ đơn giản 1 điều kiện nữa.

Toán tử logic (Logical operators) sẽ kiểm tra nhiều điều kiện cùng lúc giúp bạn. Có 3 toán tử logic trong C++:

Operator Symbol Form Operation
Logical NOT ! !x true if x is false, or false if x is true
Logical AND && x && y true if both x and y are true, false otherwise
Logical OR || x || y true if either x or y are true, false otherwise

Chú ý: Luôn sử dụng dấu ngoặc đơn () khi thực hiện với các mệnh đề quan hệ để thể hiện rõ ràng ý nghĩa dòng lệnh và hạn chế sai sót. Với cách này, bạn thậm chí không cần nhớ nhiều về độ ưu tiên toán tử.

Ví dụ:

#include <iostream> #include <iomanip> // for std::setprecision() using namespace std; int main() { cout << "Enter a number: "; int value; cin >> value; if (!value) cout << value << " is false" << endl; else cout << value << " is true" << endl; if ((value > 1) && (value < 100)) cout << value << " is between 1 and 100" << endl; else cout << value << " is not between 1 and 100" << endl; if ((value == 1) || (value == 100)) cout << value << " == 1 or "<< value <<" == 100" << endl; else cout << value << " != 1 or " << value << " != 100" << endl; return 0; }
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Outputs:

Operators

 

Toán tử trên bit trong C++ (Bitwise operators)

Toán tử trên bit dùng để thao tác với các bit trên một biến.

Tại sao cần thao tác trên bit? Trong quá khứ, bộ nhớ máy tính chưa phát triển, vấn đề về quản lý bộ nhớ là rất quan trọng. Vì vậy, người lập trình cần tận dụng tối đa các bit trong bộ nhớ.

Ví dụ: Các biến được lưu trong bộ nhớ ở một địa chỉ duy nhất, những địa chỉ này được xác định với đơn vị nhỏ nhất là byte. Xét kiểu dữ liệu bool, nó chỉ nắm giữ 2 giá trị true (1) hoặc false (0). Kiểu bool chỉ cần 1 bit để lưu trữ dữ liệu, nhưng nó lại chiếm giữ 1 byte trong bộ nhớ, vậy 7 bit còn lại sẽ là lãng phí. Sử dụng toán tử trên bit giúp bạn có thể chứa 8 biến kiểu bool vào 1 byte duy nhất, và tiết kiệm bộ nhớ đáng kể.

Trong quá khứ, sử dụng toán tử trên bit rất được ưu chuộng. Ngày nay, bộ nhớ máy tính đã phát triển và rẻ hơn, lập trình viên thường quan tâm đến tính dễ hiểu và dễ nâng cấp của mã nguồn. Do đó, việc thao tác trên bit không còn được ưu chuộng, ngoại trừ những trường hợp cần tối ưu tối đa tốc độ, bộ nhớ (những chương trình thao tác big data, những game lớn, lập trình nhúng …).

Vì các toán tử trên bit ít gặp nên mình chỉ giới thiệu qua cho các bạn tham khảo, bạn có thể tự tìm hiểu thêm nếu muốn chuyên sâu hơn.

Bảng bên dưới gồm 6 toán tử thao tác trên bit:

Operator Symbol Form Operation
left shift << x << y all bits in x shifted left y bits
right shift >> x >> y all bits in x shifted right y bits
bitwise NOT ~ ~x all bits in x flipped
bitwise AND & x & y each bit in x AND each bit in y
bitwise OR | x | y each bit in x OR each bit in y
bitwise XOR ^ x ^ y each bit in x XOR each bit in y

Bảng bên dưới gồm 5 toán tử gán trên bit:

Operator Symbol Form Operation
Left shift assignment <<= x <<= y Shift x left by y bits
Right shift assignment >>= x >>= y Shift x right by y bits
Bitwise OR assignment |= x |= y Assign x | y to x
Bitwise AND assignment &= x &= y Assign x & y to x
Bitwise XOR assignment ^= x ^= y Assign x ^ y to x

Các toán tử hỗn hợp trong C++ (Misc Operators)

Sizeof operator

Operator Symbol Form Operation
Sizeof sizeof sizeof(type)
sizeof(variable)
Returns size of type or variable in bytes

Để xác định kích thước của một kiểu dữ liệu trên một máy tính cụ thể, C++ cung cấp cho bạn toán tử sizeof. Toán tử sizeof là toán tử một ngôi, nhận vào một kiểu dữ liệu hoặc một biến, và trả về kích thước (byte) của kiểu dữ liệu hoặc biến đó. Toán tử này sizeof đã được giải thích chi tiết trong bài Số tự nhiên và Số chấm động trong C++ (Integer, Floating point).

Comma operator

Operator Symbol Form Operation
Comma , x, y Evaluate x then y, returns value of y

Các biểu thức đặt cách nhau bằng dấu phẩy (,), các biểu thức con lần lượt được tính từ trái

sang phải. Biểu thức mới nhận được là giá trị của biểu thức bên phải cùng.

Ví dụ:

x = (a++, b = b + 2); // Tương đương với a++; b = b + 2; x = b;
0
1
2
3

hoặc

int x = 0; int y = 2; int z = (++x, ++y); // increment x and y // Tương đương với int x = 0; int y = 2; ++x; ++y; int z = y;
0
1
2
3
4
5
6
7
8
9
10

Chú ý: Dấu phẩy có ưu tiên thấp nhất trong tất cả các toán tử.

Vì vậy, hai dòng code sau đây sẽ cho kết quả khác nhau:

z = (a, b); // z = b z = a, b;   // z = a, và b bị bỏ qua
0
1

Chú ý: Tránh sử dụng các toán tử dấu phẩy (,), ngoại trừ trường hợp dùng trong vòng lặp. Vấn đề về vòng lặp sẽ được hướng dẫn chi tiết trong bài Vòng lặp For trong C++ (For statements).

Conditional operator

Operator Symbol Form Operation
Conditional ?: c ? x : y If c is nonzero (true) then evaluate x, otherwise evaluate y

Toán tử điều kiện ( ?: ) là toán tử 3 ngôi duy nhất trong C++ (có 3 toán hạng), tương đương với câu điều kiện ( if/else statements ).

Cấu trúc câu điều kiện if/else:

if (condition)     // nếu condition là true      expression1;  // thực thi câu lệnh này else      expression2;  // nếu condition là false, thực thi câu lệnh này
0
1
2
3

Hoặc :

if (condition)     // nếu condition là true      x = value1;   // x = value 1 else      x = value2;   // nếu condition là false, x = value 2
0
1
2
3

Viết lại dưới dạng toán tử điều kiện ( ?: ):

(condition) ? expression1 : expression2;
0

Hoặc:

x = (condition) ? value1 : value2;
0

Ví dụ:

#include <iostream> using namespace std; int main() { int x{ 6 }, y{ 9 }, max; if (x > y) { max = x; } else { max = y; } cout << "Max = " << max << endl; // Tương đương với max = (x > y) ? x : y; cout << "Max = " << max << endl; return 0; }
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Outputs:

Operators

Chú ý: Chỉ sử dụng toán tử điều kiện với những câu điều kiện đơn giản.

Ví dụ:

#include <iostream> using namespace std; int main() { int a{ 3 }, b{ 2 }, c{ 4 }, max; // Khó hiểu, dễ sai => Không nên max = a > b ? (a > c ? a : c) : (b > c ? b : c); cout << "Max = " << max << endl; // Dễ hiểu => Nên max = a; if (max < b) { max = b; } if (max < c) { max = c; } cout << "Max = " << max << endl; return 0; }
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Outputs:

Operators

Chú ý: Toán tử điều kiện ( ?: ) có thể là một biểu thức (expression), trong khi câu điều kiện ( if/else ) chỉ là một câu lệnh (statements).

Ví dụ:

bool bIsVip = true; // Initializing a const variable const double dPrice = bIsVip ? 1000 : 500;
0
1
2
3

Trong ví dụ trên, không thể dùng câu điều kiện ( if/else ) để thay thế. Vì một hằng số phải được khởi tạo giá trị tại thời điểm khai báo.

Một số toán tử khác

Operator Description
. (dot) and -> (arrow) Member operators are used to reference individual members of classes, structures, and unions.
Cast Casting operators convert one data type to another. For example, int(2.2000) would return 2.
& Pointer operator & returns the address of an variable. For example &a; will give actual address of the variable.
* Pointer operator * is pointer to a variable. For example *var; will pointer to a variable var.

Độ ưu tiên và quy tắc kết hợp toán tử trong C++

Để đánh giá đúng một biểu thức như 6 + 9 * 8, bạn cần hiểu rõ ý nghĩa mỗi toán tử trong biểu thức đó, và thứ tự thực hiện của nó. Thứ tự thực hiện của các toán tử trong 1 biểu thức gọi là độ ưu tiên của toán tử (operator precedence).

Khi áp dụng độ ưu tiên toán tử, biểu thức bên trên sẽ tương đương với 6 + ( 9 * 8 ) = 78. Phép nhân (*) có độ ưu tiên cao hơn phép cộng (+), compiler sẽ tự động hiểu và thực hiện đúng theo độ ưu tiên của từng toán tử.

Khi 2 toán tử có cùng độ ưu tiên trong 1 biểu thức, các quy tắc kết hợp (associativity rules) sẽ nói cho compiler biết nên thực hiện từ trái sang phải (L->R) hay từ phải sang trái (R->L).

Ví dụ: bạn có biểu thức 6 * 9 / 8. Trong biểu thức này, phép nhân (*) và chia (/) cùng độ ưu tiên, nhưng nó có quy tắc kết hợp từ trái sang phải (L->R). Vì vậy nó sẽ tương đương (6 * 9) / 8.

Bảng độ ưu tiên (operator precedence) và quy tắc kết hợp (associativity rules) các toán tử trong C++:

Prec/Ass Operator Description Pattern
1 None ::
::
Global scope (unary)
Class scope (binary)
::name
class_name::member_name
2 L->R ()
()
()
{}
type()
type{}
[]
.
->
++
––
typeid
const_cast
dynamic_cast
reinterpret_cast
static_cast
Parentheses
Function call
Initialization
Uniform initialization (C++11)
Functional cast
Functional cast (C++11)
Array subscript
Member access from object
Member access from object ptr
Post-increment
Post-decrement
Run-time type information
Cast away const
Run-time type-checked cast
Cast one type to another
Compile-time type-checked cast
(expression)
function_name(parameters)
type name(expression)
type name{expression}
new_type(expression)
new_type{expression}
pointer[expression]
object.member_name
object_pointer->member_name
lvalue++
lvalue––
typeid(type) or typeid(expression)
const_cast<type>(expression)
dynamic_cast<type>(expression)
reinterpret_cast<type>(expression)
static_cast<type>(expression)
3 R->L +

++
––
!
~
(type)
sizeof
&
*
new
new[]
delete
delete[]
Unary plus
Unary minus
Pre-increment
Pre-decrement
Logical NOT
Bitwise NOT
C-style cast
Size in bytes
Address of
Dereference
Dynamic memory allocation
Dynamic array allocation
Dynamic memory deletion
Dynamic array deletion
+expression
-expression
++lvalue
––lvalue
!expression
~expression
(new_type)expression
sizeof(type) or sizeof(expression)
&lvalue
*expression
new type
new type[expression]
delete pointer
delete[] pointer
4 L->R ->*
.*
Member pointer selector
Member object selector
object_pointer->*pointer_to_member
object.*pointer_to_member
5 L->R *
/
%
Multiplication
Division
Modulus
expression * expression
expression / expression
expression % expression
6 L->R +
Addition
Subtraction
expression + expression
expression – expression
7 L->R <<
>>
Bitwise shift left
Bitwise shift right
expression << expression
expression >> expression
8 L->R <
<=
>
>=
Comparison less than
Comparison less than or equals
Comparison greater than
Comparison greater than or equals
expression < expression
expression <= expression
expression > expression
expression >= expression
9 L->R ==
!=
Equality
Inequality
expression == expression
expression != expression
10 L->R & Bitwise AND expression & expression
11 L->R ^ Bitwise XOR expression ^ expression
12 L->R | Bitwise OR expression | expression
13 L->R && Logical AND expression && expression
14 L->R || Logical OR expression || expression
15 R->L ?:
=
*=
/=
%=
+=
-=
<<=
>>=
&=
|=
^=
Conditional (see note below)
Assignment
Multiplication assignment
Division assignment
Modulus assignment
Addition assignment
Subtraction assignment
Bitwise shift left assignment
Bitwise shift right assignment
Bitwise AND assignment
Bitwise OR assignment
Bitwise XOR assignment
expression ? expression : expression
lvalue = expression
lvalue *= expression
lvalue /= expression
lvalue %= expression
lvalue += expression
lvalue -= expression
lvalue <<= expression
lvalue >>= expression
lvalue &= expression
lvalue |= expression
lvalue ^= expression
16 R->L throw Throw expression throw expression
17 L->R , Comma operator expression, expression
Có thể bạn sẽ không hiểu phần lớn những toán tử trong bảng trên ở thời điểm này, bạn chỉ cần quan tâm đến những toán tử vừa học ở phần trên. Những toán tử còn lại bạn có thể tự tìm hiểu nếu bạn có nhu cầu đặt biệt nào đó.