Code

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