Code

2018年2月28日 星期三

Generating subsets of a set

學習筆記 of Competitive Programming's Handbook by Antti Laaksonen (5.1)

枚舉出所有子集合的方法有二:
1. 使用遞迴(recursion)
    每個元素都分為「取」「不取」
2. 利用二進位整數的位元
    「1」逐步左移(<<),進行&位元運算

*遞迴的概念比較複雜,假設集合內有{1, 2, 3},我們預設一個subset是空集合,先從1開始,1可以取或不取,先討論不取的情況,再看2,2也有取與不取,3也有。

所以第一個情況應該是:1不取、2不取、3不取
三個元素都討論過後,就印出該子集(此情況為空集合{}

接下來我們取3,2不取、1不取,印出{3}
會先取3的原因是,呼叫3的函式第一個返回接續
再來3會被pop掉,因為3是最後一個元素

接著返回呼叫2的函式
取2,1、3不取,印出{2}
注意,再來2還不會pop掉,因為2是第二個元素,後面還有3
所以接下來是取2取3,1不取,印出{2, 3}
pop 3、pop 2

照著相同的模式,接下來會是:
{1}
{1, 3}
{1, 2}
{1, 2, 3}
總共八組(2^3)
(PS. 取1的情況,可以觀察到後面2跟3的選取方式跟上面一模一樣,因為是遞迴嘛!)
如果覺得描述很抽象,可以對照著程式碼畫出樹狀圖,一步一步觀察


*第二個方法也很有趣,我們可以先看二進位中 0 到 111 的數字:
000、001、010、011、100、101、110、111
每一個位元視為一個元素,1為取,0為不取,這八個數字就代表所有子集了!
{}、{1}、{2}、{1, 2}、{3}、{1, 3}、{2, 3}、{1, 2, 3}

實作的方法:用位元AND(&)判斷該位元為1或為0,以及用左移運算子(<<)逐步比較所有位元

========================================

*程式碼以C++為例:

#include <iostream>
#include <vector>
#include <set>
#include <cmath>
using namespace std;

vector<int> subsets; //m1's subsets 的 index

void m1(int k, vector<int> s){
    int n = (int)s.size();
    
    if (k == n) { //印出子集
        cout << "{";
        bool f = false;
        for (auto it : subsets) {
            if (f) cout << ",";
            f = true;
            cout << s[it];
        }
        cout << "}" << endl;
    }
    
    else { //key point!
        m1(k+1, s); //不取
        subsets.push_back(k);
        m1(k+1, s); //取
        subsets.pop_back();
    }
}

void m2(vector<int> s){
    int n = (int)s.size();
    
    for (int i = 0; i < (1<<n) ; i++) { //key point!
        cout << "{";
        bool f = false;
        for (int j = 0; j < n; j++) {
            if (i & (1<<j)) { //key point! 1左移
                if (f) cout << ",";
                f = true;
                cout << s[j];
            }
        }
        cout << "}" << endl;
    }
}

int main(int argc, const char * argv[]) {
    vector<int> s = {0, 5, 7};
    m1(0, s);
    cout << "=========" << endl;
    m2(s);
    return 0;
}

2018年2月24日 星期六

Intersection of two integer arrays

學習筆記 of Competitive Programming's Handbook by Antti Laaksonen (4.6)

求兩個整數陣列v1, v2 的交集,有三種方式可以實現:
1. 將其中一個陣列設成set,再比較另一個陣列的所有元素 ( O(nlogn)
2. 將其中一個陣列設成unordered_set,再比較另一個陣列的所有元素( O(n)
3. 兩個陣列都做sorting,再一一比較各個元素( O(nlogn)
    (p.s. sorting: O(nlogn),其他: O(n))

set 與 unordered_set的差別在於,set奠基於平衡二元樹,unordered_set奠基於雜湊法,前者可保有順序與一些獨有函數功能,而後者效率較好

特別的是,單看複雜度,第二個方法是最快的,然而,實際操作卻發現複雜度為O(nlogn)的第三個方法才是最省時的!(三比二快了兩倍)
更好玩的是,雖然一和三都是O(nlogn),三卻比一快了近十倍!
理由在於,雖然第三個方法是 O(nlogn),卻只要在開始時做一次sorting,其他動作便都是線性的了
而第一個方法則因為要在整個程序中維持複雜的平衡二元樹的結構,需花費較多時間

以下以C++實作三種方法m1, m2, m3:
(p.s. 新學到的好用關鍵字:auto,用來迭代set或vector等結構很方便)
(關於auto可參考這篇好文章:C++ 程式語言 auto 自動變數類型語法教學與範例

#include <iostream>
#include <vector>
#include <set>
#include <unordered_set>
using namespace std;

set<int> v3; //intersection

void m1(vector<int> &v1, vector<int> &v2){
    set<int> s;
    for (auto it : v1) s.insert(it);
    
    for (auto it : v2)
        if (s.count(it)) v3.insert(it);
}

void m2(vector<int> &v1, vector<int> &v2){
    unordered_set<int> s;
    for (auto it : v1) s.insert(it);
    
    for (auto it : v2)
        if (s.count(it)) v3.insert(it);
}

void m3(vector<int> &v1, vector<int> &v2){
    sort(v1.begin(), v1.end());
    sort(v2.begin(), v2.end());
    
    for (auto it1 = v1.begin(), it2 = v2.begin();
         it1 != v1.end() && it2 != v2.end(); it1++) {
        while (*it1 > *it2) it2++;
        if (*it1 == *it2) {
            v3.insert(*it1);
            it2++;
        }
    }
}

void print_v3(){
    for (auto it : v3) cout << it << " ";
    cout << endl;
}

int main(int argc, const char * argv[]) {
    vector<int> A = {4,2,5,9,7}, B = {5,2,3,9};
    m1(A, B);
    print_v3(); v3.clear();
    m2(A, B);
    print_v3(); v3.clear();
    m3(A, B);
    print_v3();
    return 0;
}

2018年2月17日 星期六

Quick Sort

Reference: Comparison Sort: Quick Sort(快速排序法) Posted by Chiu CC (很棒很詳細的解說)

Quick Sort 的想法在於,選定一個元素P,用一些方法讓其他所有元素以P為基準,小於P的元素放到P的左邊,大於P的元素放到P的右邊,直到P的左邊均為小於P的元素,P的右邊均為大於P的元素,P的左右兩邊再以相同的模式處理,直到所有元素完成排列。

實現方法:
  找一個元素當作基準值pivot(方便起見,一般選擇最後一項),使用large變數走訪整個陣列並紀錄大於pivot的數的數量small變數紀錄小於pivot的數的數量
  如果large指向的元素小於pivot,表示小於pivot的數多一個,則將該元素與small指向的下一個元素(即比pivot大的第一個元素)交換(swap),small右移。
  直到large走訪完全部的元素,表示small左邊全為小於pivot的數,small右邊到large左邊均為大於pivot的數,將pivot與small下一個元素交換(達成pivot左邊均小於它,右邊均大於它的目標)
  到此,第一個pivot完成任務,接下來便是使用遞迴,將pivot左邊與右邊的所有元素都做相同的事,最後所有元素便會完成排列。

Code (C++):
#include <iostream>
using namespace std;

void quick_sort(int array[], int b, int e){ // b:begin, e:end
    if (b >= e) return;
    int sm = b-1, la = b, pi = e; // sm:small, la:large, pi:pivot
    for (; la < pi; la++) {
        if (array[la] < array[pi]) {
            swap(array[sm+1], array[la]);
            sm++;
        }
    }
    swap(array[sm+1], array[la]);
    quick_sort(array, b, sm);
    quick_sort(array, sm+2, la);
}

int main(int argc, const char * argv[]) {
    int a[6] = {2, 3, 7, 1, 9, 2}; //example
    quick_sort(a, 0, 5);
    for (int i = 0; i < 6; i++) {
        cout << a[i];
    }
    return 0;
}

2018年2月12日 星期一

Maximum subarray sum最大子字串和

Reference: Competitive Programmer's Handbook by Antti Laaksonen (2.4)

題目問的是,給一個字串,如:-1, 2, 4, -3, 5, 2, -5, 2,求連續子字串最大的和
上述例子的答案為10,因為2, 4, -3, 5, 2是總和最大的連續子字串

這題有三種解法,時間複雜度分別是O(n^3)O(n^2)O(n)

O(n^3)的解法很直觀,用三個for loop跑過每一個子字串:(function1)
第一個迴圈用來跑子字串的頭
第二個迴圈跑子字串的尾巴
第三個迴圈計算每一個子字串的總和

O(n^2)的解法是把三個for loop簡化為兩個,簡化的原理在於,第二個迴圈跑字串尾巴時就順便計算總和,過程中將最大的和記錄下來(best),如此便不需要第三個迴圈了。(function2)

O(n)的解法由J. B. Kadane想出,有時被稱為Kadane's algorithm,中心原理在於,index每到達一個元素的位置時,都確保擁有「以該位置為結尾的子字串」的最大和。
(若該和為負並且小於下一個元素的值,則被取代 。若為正數,則無論下一個元素的值是否大於它,都會累加。判斷的依據可以想成,加了下一個元素,和會不會變大?捨棄前面元素的和,只從下一個元素開始計算,能否獲得更大的和?)(function3)

#include <iostream>
#include <algorithm>
#include <limits>
using namespace std;

void function1(int array[]){
    int best = INT_MIN;
    for (int i = 0; i < 8; i++) {
        for (int j = i; j < 8; j++) {
            int sum = 0;
            for (int k = i; k <= j; k++) {
                sum += array[k];
            }
            best = max(best, sum);
        }
    }
    cout << best << endl;
}

void function2(int array[]){
    int best = INT_MIN;
    for (int i = 0; i < 8; i++) {
        int sum = 0;
        for (int j = i; j < 8; j++) {
            sum += array[j];
            best = max(best, sum);
        }
    }
    cout << best << endl;
}

void function3(int array[]){
    int best = INT_MIN, sum = 0;
    for (int i = 0; i < 8; i++) {
        sum = max(array[i], sum + array[i]);
        /*以第i-1位為結尾的子字串是否要加上第i位元素,或捨棄只取第i位元素,才能獲得最大和*/
        best = max(best, sum);
    }
    cout << best << endl;
}


int main(int argc, const char * argv[]) {
    int a[] = {-1, 2, 4, -3, 5, 2, -5, 2};
    function1(a);
    function2(a);
    function3(a);
    return 0;
}

2018年2月5日 星期一

Proof of Binet's Formula

Binet's formula 可以算出費氏數列某一項的值
以下是公式推導過程
其中𝜙(大寫phi)為黃金比例(golden ratio)
𝜑(小寫phi)為1/𝜙



2018年2月4日 星期日

UVA 10003 Cutting Sticks (DP)


DP = Dynamic Programming, 動態規劃
#思考
假設今天有一條木頭,長100
要切的位置有三個點:25, 50, 75
切割點
0
1
2
3
4
刻度
0
25
50
75
100
把木頭的兩個端點也考慮進去
題目問最小的花費(cost)
我們可以把花費當作把木頭搬上切割的工作台需要的成本(恰等於木頭長度)
木頭越長,要付給木工的錢就越多

*先假設一種情況:
把100的木頭搬上工作台,cost = 100
先在50的位置切下,剩下0到50,50到100兩段木頭
把0到50的木頭搬上,cost加上50,再切25的位置,半段木頭完成切割
50到100也一樣,搬上去,cost加50,再切75,半段完成
所以,依照這樣的切割流程,總花費是100+50+50=200
以此類推,也可以先切25,再切75,再切50,etc
三個切割點共有3! =6種排列
但問題是,我們不可能真的每一種排列都去嘗試,那樣太沒效率
這時候要使用DP
但問題是,我們怎麼知道這題要用DP解?

#DP的主要構成元素有二:
1. Optimal Substructure(最優子結構):
    大問題可以分解成小問題,小問題可以再分解成小小問題...直到最小的問題可以直接算出來,再逆推得到大問題的答案。
    其實這也就是分治法(Divide and Conquer)啦!
2. Overlapping Subproblem(重覆子問題):
    有很多重複的小問題

*怎麼看出這題切木頭有最優子結構?
可以這樣想:
長100的木頭,可以看成兩段50的木頭
這兩段50的木頭分別做切割,可以找到最小花費
50的木頭可以再看成兩段25的木頭,0到25、25到50
但是25的木頭不能再切割了,所以花費是0
所以切割長度50的木頭,你只需花費搬上工作台的成本50
兩段50的木頭,cost = 50+50
(另一段同理,50到75、75到100,一樣都不能再切割)
別忘了還有搬上100木頭的成本100
你看,花費的加總不正是這個切割情況下的答案嗎?
當然,這是先切50的情形,其他情況也是以此類推
從這個角度來看,最優子結構已經浮出表面了

*那重複子問題呢?
那就這樣想:
先切75,就看成0到75跟75到100
75到100不能再切
0到75如果先切50,可以看成0到50跟50到75兩段木頭
發現了嗎?小問題重複了!

DP的精髓就在於此:
因為有很多重複的小問題
如果每次都重新算過一遍,那跟所有排列都算過一遍根本沒什麼不一樣
但如果,每一次算出小問題的答案就把它記錄下來
下次再遇到有需要計算那個小問題的答案時只要查表,就可以直接拿來用
效率不就大大提升了嗎

好,所以我們現在已經知道,這題用DP來解是個好主意
那怎麼寫才能把所有情形都跑過一次呢?

先令木頭長度為l(length),切割點數量n
cost建表為dp[n][n](n最大不超過50,可直接設成dp[52][52]),比如dp[0][2],紀錄的就是0到50的最小花費
table紀錄每個切割點的刻度,這樣才方便計算未切割前搬上工作台要花的cost
r(range)是區間範圍,b(begin)是起始點,e(end)是終點


#include <iostream>
#include <climits>
using namespace std;

int main(int argc, const char * argv[]) {
    int l, n;
    while (cin >> l && l != 0) {
        cin >> n;
        int dp[52][52] = {0};
        int *table = new int[n+2];
        n++;
        table[0] = 0; table[n] = l;
        for (int i = 1; i < n; i++) cin >> table[i];
        int r, b, e, c, temp;
        for (r = 2; r <= n; r++) {
            for (b = 0; b < n; b++) {
                e = b + r;
                if (e > n) break;
                dp[b][e] = INT_MAX;
                for (c = b+1; c < e; c++) {
                    temp = dp[b][c] + dp[c][e] + table[e] - table[b];
                    if (temp < dp[b][e]) dp[b][e] = temp;
                }
                
            }
        }
        printf("The minimum cutting is %d.\n", dp[0][n]);
    }
    return 0;
}
}

程式碼看起來還是很抽象。
但你可以按部就班、土法煉鋼的去理解它----跟著程式碼一步一步走,看它怎麼走的
ex前面的例子:長100的木頭










像這樣一步一步去觀察
每一次temp算出來的答案如果小於紀錄值,dp就會被temp取代
r從2開始的原因在於,r=1時,所有cost都等於0(因為不用切)
之後你會發現,這個code的DP是使用Bottom-Top:
從最小區間開始填,大的區間會用到小區間的答案
之後再來補充Top-Bottom的解法

2018年1月30日 星期二

UVA 10154 Weight and Measure (Top-Down)

這題的概念是想辦法找出可以堆疊的最大烏龜數
可以使用Minimizing Tardy Jobs的技巧
(把烏龜所能乘載的最大重量(carry)當作Due date, 烏龜的重量當作processing time.)

(有興趣可以參考我之前的文章 Single Machine Scheduling - Minimizing Number of Tardy Jobs)

概念是將烏龜從頂端開始往下放,因為越下面的乘載量應該要越大,所以將烏龜依照carry能力由小排到大。(carry = strength - weight)

原理基本上就是因為烏龜已經依照承載量排序,如果原本最底下的烏龜可以承受整個重量,那麼最新的烏龜一定也可以取代最底下的烏龜承受整個重量(越後面的烏龜能力越好)。而且由於當我們在挑最新的烏龜的時候,是先將最新的烏龜放到最底下,所以暫時不用考慮他的重量是不是會導致整個重量太重(會在之後放新的烏龜時考慮)。

所以如果新的烏龜撐得住全部,那麼就直接加入(Size++)。
如果不行,那麼再從中把最重的烏龜踢掉。也就是新的烏龜一定放的進來(能力比較好),那麼我們就整個stack當中踢掉最重的烏龜,然後補這隻烏龜進來。整個Stack少了最重的烏龜,加了新的烏龜,Size不變,但總重變輕 -> 有優化的趨勢,算一種greedy method.

整個步驟大概是:

  1. 將烏龜依照carry能力排序
  2. 創立一個stack來存放堆疊的烏龜
  3. 依照順序將烏龜放入stack (每一次都要更新總重量)
  4. 如果新的烏龜的乘載量大於目前的總重量,就將他直接放入。如果新的烏龜的乘載量小於目前的總重量,就找出目前堆疊中最重的烏龜,把他移除。(注意!因為我們要盡可能讓stack的總重輕一點,找最重的烏龜的時候要包含這隻最新的烏龜,所以實作時建議先放入再挑出烏龜。)
//  main.cpp
//  UVA_10154_Weight_and_Measure
//
//  Created by Darklanx on 2018/1/31.
//  Copyright © 2018年 Darklanx. All rights reserved.
//
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
struct Turtle{
    int weight, carry;
    Turtle(int w, int strength){
        weight = w;
        carry = strength - weight;
    }
};

bool sort_by_carry_asc(Turtle t1, Turtle t2){
    return t1.carry < t2.carry;
}
vector<Turtle>::iterator find_heaviest(vector<Turtle>& turtles){
    vector<Turtle>::iterator fat_turtle;
    int max =0;
    for(vector<Turtle>::iterator it=turtles.begin();it!=turtles.end();it++){
        if(it->weight > max){
            max = it->weight;
            fat_turtle = it;
        }
    }
    return fat_turtle;
}

//sort by (strength - weight) in ascending order
//start adding turtles (Top-down)
//if: (strength >= total_weight) add that damn turtle (stack++)
//else: find the heaviest turtle, kick him out(stack+=0)

int main() {
    int iweight, istrength;
    //int max_stack =0;
    vector<Turtle> turtles;
    vector<Turtle>::iterator it;
    //vector it
    //input turtles
    while(cin >> iweight >> istrength){
        Turtle t(iweight,istrength);
        turtles.push_back(t);
    }
    //sort
    sort(turtles.begin(),turtles.end(),sort_by_carry_asc);
    int total_weight=0;
    it = turtles.begin();
    vector<Turtle> stacking;
    while(it != turtles.end()){
        //!!!!
        //一定要先把最新的烏龜推進去 假設最新的烏龜是最重的烏龜 下面再移除的時候才有辦法把它移除
        stacking.push_back(*it);
        
        if(it->carry >= total_weight){// if can carry
            //max_stack ++;
            total_weight += it -> weight;
        }else{// too heavy
              //remove the heaviest
            total_weight -= find_heaviest(stacking) -> weight;
            stacking.erase(find_heaviest(stacking));
            total_weight += it -> weight;
        }
        it++;
    }
    
    cout << stacking.size() << endl;
    return 0;
}