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;
}



2018年1月29日 星期一

Single Machine Scheduling - Minimizing Number of Tardy Jobs

這裡介紹如果一系列工作有著不同的Due date跟Process Time。

因為我懶得附圖跟例子所以大家可以參考演算法筆記工作排程問題(其實他也講得很詳細,只是如果有人不懂為什麼他那樣可以保證新加入的工作不會超時可以繼續讀下去)。

那麼要如何排序才能讓我們的Tardy Job最少(超時的工作最少,也就是在期限內完成最多工作)
首先將所有工作依照Earliest Due Date (EDD)做排序,然後從Due Date最早的工作開始加入行程。
一旦新加入的工作無法在期限內完成,就砍掉行程中最耗時的工作。
如此一來最後得到的結果就是完成最多工作的情況。
證明;
假設當前的狀態沒有任何超時,在加入新工作時可以分為兩種情況

  1. 沒超時,工作量+1
  2. 超時,剔除耗時最大的工作,加入這個新工作。工作量+1-1維持不變,但是因為剔掉了耗時最大的工作使得總工作時間變短,未來加入的工作將更不易超時。
這裡有人可能會有一個疑問就是,怎麼確保當我們砍掉耗時最大的工作時,新加入的工作一定不會過期?
重點在於,所有的工作是照著EDD做排序的!!!!
假設新加入的工作X是最耗時的工作,那麼他就被砍掉不加入,所以不影響。
假設新加入的工作X不是最耗時的工作,也就是說有一個工作L耗時更大 => PL > PX
因為 PL在所有工作裡面  而這一系列的工作沒有超時。因此當一系列的工作砍掉長度較長的L後,再加入長度較短的X,一定也不會超出原本的期限Deadline1。而X的期限DeadlineX因為照著EDD做排序,所以會在Deadline1之後,所以這樣操作一定可以保證X會在期限內完成。


Single Machine Scheduling - Shortest Processing Time

Shortest Processing Time (SPT) 是用來處理當任務被完成之前我們必須為之付出代價(Inventory Cost)。因此我們要盡早完成他們,也就是讓他們的Flow Time最小。
(Flow Time 為完成時間減去任務出現的時間。也就是Fi = Ci - ri, Fi as Flow time, Ci as Completion time, ri as release time.)

假設我們有任務:
Job 1 2 3 4 5
Process Time:   4 2 3 6 1
此時如果我們的Sequence是照著1-2-3-4-5
那麼Flow Time就會是Job1的完成時間+Job2的完成時間+.....Job5的完成時間
也就是 4 + (4+2) + (4+2+3) + (4+2+3+6) + (4+2+3+6+1) = 50

如果我們從最小的work time開始選擇(SPT) 也就是照著 5-2-3-1-4的順序
(SPT的概念就是擁有最短Process Time的先處理)
那麼Flow Time就會變成 1 +  (1+2) + (1+2+3) + (1+2+3+4) + (1+2+3+4+6) = 36
我們會發現整整少了50-36=14的Flow Time。
那麼要怎麼證明SPT出來的結果一定是最少Flow Time?
Process Time Pi < Pj
T(A) T(B)為前後Flow Time的總和 我們只看中間Pi Pj的部分:
假設 S1 是最佳解 
    T(A) + Pi + (Pi + Pj) + T(B)  
假設 S2 不是最佳解
    T(A) + Pj + (Pj + Pi) + T(B)
此時S1 S2的Flow Time差 S1 - S2 = Pi - Pj  < 0 ,由此可知必須將小的Pi放在前面。 

到這裡再重複一個觀念就是雖然不論用甚麼順序,總共的工作時間都是所有Process Time的總和。(因為只有一台機器 不管用甚麼順序 機器完成所有工作的時間都是固定的)
但是SPT要求的是最少的Flow Time,也就是任務出現後(上述假設任務同時出現)要盡可能早點完成。
比如說,今天有五個客人同時來點餐,雖然我們把五份餐點準備好的時間是固定的。此時Flow Time代表的卻是每一份單子被完成之前等待的時間,因此總Flow Time越短,也就代表客人等待的時間的加總越短。


UVA 11389 The Bus Driver Problem

題目的概念很簡單 讓司機盡可能不要超時即可
所以讓白天的工作由小排到大 晚上由大排到小
相加即可讓司機的工作盡量不要超時
這邊給一個小提醒就是 reverse是翻轉 不是降冪排序QAQ
要先sort才reverse才是降冪
花了我一大票時間找這個bug.....也是耍了腦殘

#include <iostream>
#include <algorithm>
using namespace std;
int main(){
	unsigned int n, d ,r;

	while(cin >> n >> d >> r && (n || d || r)){

		int Day[n], Night[n];	
		for(int i=0;i<n;i++) cin >> Day[i];
		for(int i=0;i<n;i++) cin >> Night[i]; 	
		sort(Day,Day+n);
		sort(Night,Night+n);
		reverse(Night,Night+n);
		int Add[n];
		int total = 0;
		for(int i=0;i<n;i++){
			Add[i] = Day[i] + Night[i] - d;
			if(Add[i] > 0 ){
				total += Add[i] * r;
			}
		}
		cout << total <<endl;
	}
	return 0;
}

UVA 10037 Bridge

過橋問題
必須將問題分
  1. 一個人
  2. 兩個人
  3. 三個人
  4. 四個人以上
(下文中的 A B C D 中都是 A最快D最慢的排序)
不論是一個人或是兩個人都可以直接走過去 .
A B C三個人的話則可以
A C去 A回 A B去 或是
A B去 A回 A C去
依照題意uva好像兩種答案都可以接受?(時間一樣)

再來先看四個人 A B C D
因為D是最慢的 所以我們可以知道D的time是避不開的
那麼要讓D過去又能得到最快的解答
要不就是讓AD過去 A回來
或者是讓 CD 一起過去讓C的時間被D蓋過去
可是這時候因為必須讓C再回來 就沒有真的節省到C的時間
因此必須讓AB先過去A回來後再讓CD過去 然後讓B代替他們回來
因此就得出四個人中的唯一兩種解法
A D -> A -> AC -> A -> AB
或是
A B -> A -> CD -> B -> AB
我們會發現這兩種解法都是找出讓CD一起過去的最佳方案
所以就可以假設 當人數(n)大於等於四的時候
運用DP的概念 先讓最後兩個人過去 慢慢把n削減
如果n原本是奇數 那麼最後 n 會等於 3 再套三人的算法
如果n原本是偶數 那麼最後n會等於2 再套兩人的算法
如此一來就可以解決了

不過我覺得實在是很難理解或證明為什麼就是不斷讓最後兩個人過去可以形成最佳解
網路上對這題的討論蠻多的 但主要都是直接引用這個公眾解法
有另一個有趣的方法是使用F(n)去判斷一次過去一個人或過去兩個人
然後分別判斷F(n-1) & F(n-2)的狀態 有興趣可以參考下面這篇文
運用DP處理UVA10037

如果有人有很直觀的方法可以解釋如何想到以及理解這種解法的 歡迎不吝賜教

#include <iostream>
#include <algorithm>
#include <string.h>
#include <vector>
#include <sstream>
using namespace std;
#define f1 (0)  //fastest 1
#define f2 (1)  // fastest 2
#define s1(n) n-1   //slowest 1
#define s2(n) n-2   // slowest 2

int main() {
    unsigned int TC, n, time[1000];
    cin >> TC;
    while(TC--){
        cin >> n;
        int people = n; //幾個人
        int total_time=0;
        vector steps;
        for(int i = 0; i < n; i++){
            cin >> time[i];
        }
        sort(time,time+n);
        //first count time
        while(n>3){ // multi-dudes
            //AB A CD B -> A+ 2B + D
            int t1 = time[f1] + 2*time[f2] + time[s1(n)];
            //AD A AC A -> 2A + C + D
            int t2 = 2*time[f1] + time[s2(n)] + time[s1(n)];
            if(t1 < t2){
                total_time += t1;
            }
            else{
                total_time += t2;
            }
            n = n-2;
        }
        if(n == 2){
            total_time += time[f2];
        }
        else if (n==3){
            total_time += time[f1] + time[f2] + time[s1(n)];
        }
        else if(n == 1){//one-man army
            total_time += time[f1];
        }
        cout << total_time <3){ // multi-dudes
            //AB A CD B -> A+ 2B + D
            int t1 = time[f1] + 2*time[f2] + time[s1(people)];
            //AD A AC A -> 2A + C + D
            int t2 = 2*time[f1] + time[s2(people)] + time[s1(people)];
            if(t1 < t2){
                printf("%d %d\n%d\n",time[f1],time[f2],time[f1]);// AB A
                printf("%d %d\n%d\n",time[s2(people)],time[s1(people)],time[f2]);//CD B
            }
            else{
                printf("%d %d\n%d\n",time[f1],time[s1(people)],time[f1]);//AD A
                printf("%d %d\n%d\n",time[f1],time[s2(people)],time[f1]);//AC A
            }
            people = people-2;
        }
        if(people == 2){
            // A B
            printf("%d %d\n",time[f1],time[f2]);
        }
        else if (people==3){
            // A B, A , A C
            printf("%d %d\n%d\n%d %d\n",time[f1],time[f2],time[f1],time[f1],time[s1(people)]);
        }
        else if (people == 1){
            printf("%d\n",time[f1]);
        }
        if(TC) cout << endl;
    
        
    }
    return 0;
}

參考概念Algorithmist

2018年1月28日 星期日

UVA 10020 Minimal coverage

#題目:
給定一個值M,和許多不同的線段
求最少的線段可完整覆蓋0到M

#思路:
從零開始要找第一個線段,該線段的左值必須在零的左邊或等於零
在這個條件下再去尋找擁有最大右值的線段,以此類推
這樣便可確保利用最少的線段填滿0到M

*需考慮無法填滿的兩個情況:
1.尋找線段時,找不到左值符合條件的線段
2.已經用完所有線段後,依然無法填滿(亦即,最大右值的線段的右值小於M)








#include <iostream>
using namespace std;

struct segment{
    int l, r;
};

int main(int argc, const char * argv[]) {
    int cases, M;
    while (cin >> cases) {
        while (cases--) {
            segment seg[100005];
            segment ans[100005];
            cin.get(); //空行
            cin >> M;
            int i = 0;
            while ((cin >> seg[i].l >> seg[i].r)) {
                if (seg[i].l == 0 && seg[i].r == 0) break; //結束
                else if (seg[i].l < 0 && seg[i].r < 0){    //兩端都小於0 不用考慮 直接設為0 0
                    seg[i].l = 0;
                    seg[i].r = 0;
                }
                i++;
            }
            
            int larger_r = 0, mark = 0, count = 0;
            /*用larger_r找有最大r的新片段,mark記錄該片段的index,count計算片段數量*/
            int a = 0; ans[a].l = 0; ans[a].r = 0;//ans紀錄每個片段的值
            
            while (larger_r < M && count < i) {
                for (int j = 0; j < i; j++) {
                    if (seg[j].l <= ans[a].r && seg[j].r > larger_r) {
                        /*新片段的l要小於等於上一個片段的r,再找最大r*/
                        larger_r = seg[j].r;
                        mark = j;
                    }
                }
                if (larger_r == ans[a].r){  //找不到的情況 表示0到M無法被完整覆蓋
                    count = 0;
                    break;
                }
                else {
                    ans[++a].l = seg[mark].l; ans[a].r = seg[mark].r;
                    count++;
                }
            }
            
            if (larger_r < M || count == 0) cout << '0' << endl;
            else {
                cout << count << endl;
                for (int i = 1; i <= count; i++) {
                    cout << ans[i].l << " " << ans[i].r << endl;
                }
            }
            cout << "\n";
        }
    }
    return 0;
}

活動參與問題 Interval Scheduling Problem


給定一系列的活動, 如何選擇才能參與最多的活動。
基本的概念是;

  1. 將活動依照結束時間排序(假設由左排到右)
  2. 從最左邊開始選擇活動,每次碰到活動而且空閒就選擇,否則就跳過該活動。
    整個想法很簡單,只是我一直很納悶為什麼這樣選出來的結果就一定會是最佳解。
可能是因為我比較駑鈍但網路上也沒有多做解釋,我自己整理出來的結果大概是這樣;

    活動越早結束,也就有越大的機會可以參與下一個活動。
    但是這就產生一個問題,如果有兩個重疊的活動,其中一個長但是早結束(下圖灰),另一個短但是晚結束(下圖紫),這時候選擇比較早結束的也可以保證最佳解嗎?

    這時候就要想到,活動的選擇是從左邊開始選過來,所以雖然長但是早結束的活動會提早開始。也就是說如果選擇這個長的活動會導致答案並非最佳解,那代表他一定有蓋過其他活動,而且蓋過的活動必須要比它早結束(下圖藍)。否則若它依舊比較晚結束,那麼它反而會影響到後面的工作(下圖咖啡)。但因為它必須比較早結束,所以如下圖藍色與灰色,此時藍色會比灰色更早被選擇。

    所以這時候就可以理解,越早結束的活動對後面的影響越小,並不會因為它的長短而影響結果。

(如果還是沒有理解就先不要看藍色,會發現即使灰色是最長的,但它還是應該優先被選擇,因為它對後面的影響最小。但是如果藍色存在,此時就不會選擇灰色,而會選擇比它更早結束的藍色,因此越早結束越優先選擇)

簡陋參考圖











演算法筆記 -- 活動選擇問題
(Youtube) Interval Scheduling ( Greedy Algorithm ) - Algorithms
Wiki - Interval scheduling

UVA 10038 Jolly Jumpers

題目要每兩個的差剛好要填滿1~n-1
也就是說所有差相加要剛好等於 ((1+n-1)*n-1 )/ 2
同時也不能有任何數字重複 所以用set去表示


#include<iostream>
#include<math.h>
#include<set>
using namespace std;
int main(){
	int n;

	while(cin >> n){

		set s;
		int total=(1+(n-1))*(n-1)/2 ;
		if(n==1){
			total = 0;
		}
		bool flag = true;
		int tmp[2]={0};
		cin >> tmp[0];
		for(int i=1;i> tmp[i%2];
 			int dif = abs(tmp[0]-tmp[1]);
			if(s.count(dif)==0){
				s.insert(dif);
				total -= dif;
			}
			else{
				flag = false;
			}
		}
		if(total == 0 && flag == true){
			cout << "Jolly" <
另解:(直觀)

若每一次計算的差皆為新的值,則對應陣列的值加一

最後以for檢查陣列內1~n-1的值均為一

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

int main(int argc, const char * argv[]) {
    int n, i;
    while (scanf("%d", &n) != EOF){
        int seq[3005] = {0};
        bool j_or_not_j = true, check[3005] = {0};

        scanf("%d", &seq[0]);
        for (i = 1; i < n; i++) {
            scanf("%d", &seq[i]);
            check[abs(seq[i]-seq[i-1])]++;
        }

        for (i = 1; i < n; i++) {
            if (check[i] != 1) {
                j_or_not_j = false;
                break;
            }
        }

        if (j_or_not_j)
            printf("Jolly\n");
        else
            printf("Not jolly\n");

    }
    return 0;
}

UVA 495 Fibonacci Freeze

#include<stdio.h>

#define MAX 5001
int main(int argc, const char * argv[]) {

    int n, i, j, k;

    int ans[MAX][3001] = {0};

    ans[0][3000] = 0;

    ans[1][3000] = 1;

    for (i = 2; i <= MAX-1; i++) {

        for (j = 3000; j >= 0; j--) {

            ans[i][j] = ans[i-1][j] + ans[i-2][j];

        }

        for (j = 3000; j >= 1; j--) {

            if (ans[i][j] >= 10) {

                ans[i][j-1] += ans[i][j]/10;

                ans[i][j] %= 10;

            }

        }

    }

    while (scanf("%d", &n) != EOF) {

        printf("The Fibonacci number for %d is ", n);

        if (n == 0) {

            printf("0\n");

        }

        else {

            for (k = 0; ans[n][k] == 0; k++);

            for (j = k; j <= 3000; j++) {

                printf("%d", ans[n][j]);

            }

            printf("\n");

        }

    }

    return 0;
}
此code在UVa可Accepted,但在電腦上執行,可能會出現EXC_BAD_ACCESS的error

理由是電腦沒辦法分配出太大的連續記憶體(5001*3001*4 bytes)

解決方法應該是使用動態配置

UVA 11572 Unique Snowflakes

題目求最大的不連續數量
設一個陣列記錄每一次輸入值的位置
再設一個基準值在每一次重複後,更新為前面重複值的後一位,亦即陣列記錄值加一
基準值存在意義為,每一次重複後,前面重複值以前的數均不考慮
故新輸入值所對應的陣列值需大於等於基準值,才能算重複
而每一次重複後都要計算當前的長度,記錄在ans陣列
另外,必須考慮沒有重複的情況,以count紀錄
而每一次重複後,count的值也會更新
#include <stdio.h>
#define MAX 1000005

int main(int argc, const char * argv[]) {

    int case_number, n, test;

    while (scanf("%d", &case_number) != EOF) {

        while (case_number--) {

            int i, j = 0;

            int left = 1, count = 0, table[MAX] = {0}, ans[MAX] = {0}, result = 0;
            /*left紀錄基準線,count計算不重複的情況,
            table是對照表,ans是每一次的答案,result取最大的ans值*/
            scanf("%d", &n);

            for (i = 1; i <= n; i++) {

                scanf("%d", &test);

                if (table[test] >= left){  //大於等於不重複的基準線,即重複
                    ans[j++] = i - left;   //紀錄答案
                    left = table[test] + 1;  //調整基準線
                    count = i - table[test];  //記錄之後連續狀況的count亦要重新計算
                }

                else
                    count++;  // 沒重複 直接加

                table[test] = i; //更新table的值
            }

            if (count) //這個情況紀錄的是 從上一次重複(或不曾重複)到結束都沒有重複
                ans[j] = count;

            for (i = 0; ans[i]; i++) {  //找最大
                if (ans[i] > result)
                    result = ans[i];
            }

            printf("%d\n", result);
        }
    }
    return 0;
}