2017 交大競技程式訓練冬令營

Day1

Outline

  • 輸入與輸出
  • 演算法的複雜度
  • 二分搜尋演算法
  • 排序演算法

輸入與輸出

C++中兩大類輸入方式

  • stdio

  • iostream

stdio

scanf / printf

#include <cstdio>

int main(){
    int a;
    long long b;
    unsigned long long c;
    char s[100];
    double d;

    scanf("%d-%lld-%llu %s %lf",
        &a, &b, &c, s, &d);

    printf("%10d\n", a);
    printf("%10lld\n", b);
    printf("%10llu\n", c);
    printf("%10s\n", s);
    printf("%10f\n", d);
}


// input
1-2-3 abc 7.122
//output
         1
         2
         3
      abcd
  7.122000

符號 內容 範例
%i %d 整數 -7122
%u 無號整數 7122
%f 浮點數 71.22
%e %E 科學記號(E是大寫) 7.122e+4 / 7.122E+4
%c

字元

J
%s 字串 JIXX
%% %字元 %

常用scanf/printf 符號表

gets / puts

#include <stdio.h>

int main(){
    char s [256];
    gets(s);
    puts(s);
}

//input
1 2 3 4 5 6 7 8 9 10 11
//output
1 2 3 4 5 6 7 8 9 10 11

fgets / fputs

#include <stdio.h>

int main(){
    char s [256];
    fgets(s, 256, stdin);
    fputs(s, stdout);
}

為了防止溢位

現代人都應該改用fgets!!!

getchar / putchar

#include <stdio.h>

int main(){
    char c;
    c = getchar();
    putchar(c);
}

輸入優化 - 用 getchar 輸入整數

template <class T>
bool input(T& a){
    a=(T)0;
    register char p;
    while ((p = getchar()) < '-')
        if (p==0 || p==EOF) return false;
    if (p == '-')
        while ((p = getchar()) >= '0') a = a*10 - (p^'0');
    else {
        a = p ^ '0';
        while ((p = getchar()) >= '0') a = a*10 + (p^'0');
    }
    return true;
}
template<typename T, typename ...Args>
inline bool input(T &x, Args &...args){
    return input(x)&&input(args...);
}

追求更快的輸入優化

char readchar(){
    static const int bufsize = 1<<16;
    static char buf[bufsize], *p=buf, *e=buf;
    return (p==e &&
        (e=(p=b)+fread(buf,1,bufsize,stdin),p==e)?0:*p++);
}

更快的 getchar()!!!

iostream

std::cin / std::cout

#include <iostream>
using namespace std;
int main(){
    ios_base::sync_with_stdio(false); 
    cin.tie(0);
    int n;
    cin >> n;
    cout << n << endl;
    return 0;
}

cin/cout 由編譯器幫你判斷型別,省去判斷型別的時間和麻煩

ios_base::sycn_with_stdio

cin/cout 平常為了和stdio同步,所以速度會受到影響,關閉同步期望可以讓 cin/cout 的速度變快

把同步關掉之後,同時使用stdio 和 iostream 會發生不可預期的問題,請特別注意

cin.tie(0)

cin/cout 平常是互相綁定的,呼叫 cin 就會自動清空 cout 的緩衝區

cin.tie(0) 將會讓 cin / cout 解除綁定,但是要注意 <<endl 依然會清空緩衝區,必須改成 <<'\n' 才會有優化的效果

當需要輸出大量的行數的時候會差很多

std::getline - 輸入 string

#include <iostream>
#include <string>

int main ()
{
  std::string name;

  std::cout << "Please, enter your name: ";
  std::getline(std::cin, name);
  std::cout << "Hello, " << name << "!\n";

  return 0;
}

std::istream::getline()

#include <iostream>

int main () {
  char name[256],;

  std::cout << "Please, enter your name: ";
  std::cin.getline (name,256);
  std::cout << "Hello, " << name << "!\n";

  return 0;
}

Example

輸入 T 筆測資

int main(){
    int t;
    scanf("%d",&t);
    while(t--){
        solve();
    }
}
int main(){
    int t;
    cin>>t;
    while(t--){
        solve();
    }
}

以 0 結尾的測資

int main(){ 
    while ( scanf("%d",&n) && n!=0 ){
        solve(n);
    }
}
int main(){ 
    while ( cin>>n && n!=0 ){
        solve(n);
    }
}

讀到 EOF(檔案結尾字元)的測資

int main(){
    while ( ~scanf("%d %d %d", &a, &b ,&c) ){
        solve(a,b,c);
    }
}
int main(){
    while ( cin>>a>>b>>c ){
        solve(a,b,c);
    }
}

不知道一行有幾個數字的測資

#include <cstdio>
#include <cstring>

int main(){
    char s[256];

    while ( fgets(s,256,stdin) != NULL ){
        char *ptr = strtok(s," \n");
        int cnt=0, a;
        do{
            sscanf(ptr, "%d", &a)
            cnt++;
            ptr = strtok(NULL," \n");
        }while (ptr!=NULL);
        printf("%d\n",cnt);
    }
}

不知道一行有幾個數字的測資

#include <iostream>
#include <sstream>
#include <string>
using namespace std;

int main(){
    string s;
    while(getline(cin, s)){
        stringstream ss(s);
        int cnt=0, a;
        while(ss>>a)cnt++;
        cout<<cnt<<endl;
    }
}

輸出到小數點後第3位(四捨五入)

#include <cstdio>
int main(){
    float pi = 3.1415926;
    printf("%.3f\n", pi); //3.142
}
#include <iostream>
#include <iomanip>
using namespace std;
int main(){
    float pi = 3.1415926;
    cout<<fixed<<setprecision(3);
    cout<<pi<<endl; //3.142
}

輸出一個5位數整數,不足補0

#include <cstdio>
int main(){
    int n = 7122;
    printf("%05d\n", n); //07122
}
#include <iostream>
#include <iomanip>
using namespace std;
int main(){
    int n = 7122;
    cout<<setfill('0');
    cout<<setw(5)<<n<<endl; //07122
}

時間複雜度

如何估計程式跑多快?

Big O

描述函數漸進行為的數學符號

Big O

\forall n
n\forall n
then\ f(n) = O(g(n))
then f(n)=O(g(n))then\ f(n) = O(g(n))
if\ \exists\ c,C
if  c,Cif\ \exists\ c,C
s.t.\ n > C\ and\ |f(n)| \leq |cg(n)|
s.t. n>C and f(n)cg(n)s.t.\ n > C\ and\ |f(n)| \leq |cg(n)|
  • O(100n) 等價 O(n)

  • O(nlgn+n) 等價 O(nlgn)

  • O(50n^2+10n+100) 等價 O(n^2)

常見的時間複雜度

  • O(nlgn)
  • O(n)
  • O(lgn)
  • O(2^n)
  • O(n+Qlgn)
  • O(1)
  • ...

偷偷告訴你,現在電腦一般來說一秒可以跑 5*10^8

有些10^7、有些10^9

二分搜尋法

如何快速地找東西?

一個一個找 - O(N)

如果東西已經

按照大小排好了呢?

二分搜尋法

先找中間

每次直接把搜尋範圍砍一半

Binary Search

#include <stdio.h>

const int INF = 1<<29;

bool ok(int x){
    if (x>=100)return 1;
    else return 0;
}

int main(){
    int l=0, r=INF;

    while (l!=r){
        int mid = (l+r)/2;
        if ( ok(mid) )r=mid;
        else l=mid+1;
    }
    printf("%d\n",l);
}

尋找一個數字(以100為例)

時間分析

假設範圍為 C

每次範圍會縮小一半

所以總共要搜尋 lg C 次

複雜度就是 O(lg C)

排序演算法

常見的排序演算法

  • 氣泡排序法(bubble sort)
  • 選擇排序法(selection sort)
  • 插入排序法(insertion sort)
  • 快速排序法(quick sort)
  • 合併排序法(merge sort)
  • 堆排序法(heap sort)

氣泡排序法

讓最輕的東西像氣泡一樣浮上來的排序法

(雖然實作時好像都是讓最重的沉下去)

需要兩層迴圈所以是 O(N^2)

Bubble Sort

int bubble_sort(int a[], int n){
    for (int i=0; i<n ;i++){
        for (int j=0; j<n-1; j++){
            if (a[j] > a[j+1]) swap(a[j],a[j+1]);
        }
    }
}

選擇排序法

選一個最小的拿出來

選N次就可以把N個東西排好序

因為要選N次

每次都要全部看過才知道誰最小

所以是 O(N^2)

Selection Sort

int selection_sort(int a[], int n){
    for (int i=0; i<n; i++){
        for (int j=i+1; j<n; j++){
            if (a[i] > a[j]) swap(a[i],a[j]);
        }
    }
}

插入排序法

每次拿一個東西

然後插入到他應該在的位置

(平常我整理書櫃都用這個方法XD)

把東西插入他應該在的位置需要看過一遍

還要把後面的東西往後挪

所以 O(N^2)

Insertion Sort

int insertion_sort(int a[], int n){
    for (int i=0; i<n; i++){
        for (int j=i; j>0; j--){
            if (a[j-1] > a[j]) swap(a[j-1],a[j]);
            else break;
        }
    }
}

快速排序法

聽起來就很快的排序法(?

隨機找一個數字放在中間

把大於他的放右邊,小於他的放左邊

然後遞迴

T(N) = 2*T(N/2) + O(N) = O(nlgn)

 

Quick Sort

void quick_sort(int l, int r, int a[]){
    if (l>=r) return;
    int p = l;
    for (int i=l+1; i<=r; i++){
        if (a[i] < a[p]) swap(a[++p], a[i]);
    }
    swap(a[l], a[p]);

    quick_sort(l,p-1,a);
    quick_sort(p+1,r,a);
}

合併排序法

運用分治法(Divide and Conquer)

把序列切成一半,各別排好序

用O(N)合併兩個排好序的結果

也就是比較兩個序列的頭來合併

T(N) = 2*T(N/2) + O(N) = O(NlgN)

Merge Sort

void merge_sort(int l, int r, int a[], int tmp[]){
    if (l>=r) return;
    int mid = (l+r)/2;
    merge_sort(l,mid,a,tmp);
    merge_sort(mid+1,r,a,tmp);

    int p=mid+1, q=l;
    for(int i=l; i<=mid; i++){
        while(p<=r && a[i]>a[p])tmp[q++] = a[p++];
        tmp[q++] = a[i];
    }
    while(p<=r)tmp[q++] = a[p++];
    for(int i=l; i<=r; i++)a[i] = tmp[i];
}

堆排序法

把序列變成一個Heap

(Heap 是一個可以找最大值的資料結構)

再依序把東西從Heap裡拿出來

有點像選擇排序法

但是把找最大值的時間變成O(NlgN)了

其中 Heap的原理有點像氣泡排序法

只是從一條直鍊變成一顆完全二元樹

時間為 N*O(NlgN) = O(N lgN)

Heap Sort

void heap_sort(int a[], int n){
    for(int i=0; i<n; i++){
        int w=i;
        while(w>0){
            if(a[(w-1)/2]<a[w])std::swap(a[(w-1)/2],a[w]);
            w/=2;
        }
    }
    for (int m=n-1; m>0; m--){
        std::swap(a[0],a[m]);
        int w=0;
        while (1){
            int tw=w;
            if (w*2+1<m && a[w*2+1]>a[tw])tw=w*2+1;
            if (w*2+2<m && a[w*2+2]>a[tw])tw=w*2+2;
            if (tw==w)break;
            std::swap(a[w],a[tw]);
            w=tw;
        }
    }
}
Made with Slides.com