競技プログラミング練習会

2019 Normal

第1回  プログラミング基礎(続),計算量, 貪欲法

担当:laft

自己紹介

自己紹介

  • 京都大学工学部情報学科2回生
  • 本名:平井 雅人
  • AtCoder: かつては水色だった…(今は緑)
  • CとC++を普段使ってます。
  • 自分含めて2人交代で回していく予定です。
  • よろしくお願いします。

KMC-ID:laft

皆さんの自己紹介もお願いします。

  • 名前(部員の方はKMC-ID)
  • 所属(大学など?)
  • 使える言語や使う予定の言語
  • プログラミング経験や競プロの経験(もちろんなくても大丈夫です!)
  • 趣味・特技・意気込みなど

あくまで一例ですが、下を参考に

今日の内容

今日の内容

  • プログラミング基礎(続)
    • 型について
    • 配列(vector)
    • 文字列(string)
    • 関数
  • 計算量
  • 貪欲法

型の補足事項

型とは(おさらい)

  • 変数宣言時に設定する、変数の形式のこと。
  • int型(整数),double型(小数)などがあると話した。
  • もう少し深く、この型について掘り下げる。

bitについて

  • コンピューターの中で用いられるデータの最小単位
  • 1bitで0か1を表せる。
  • 1byte=8bit
  • 内部的にはあらゆるデータはbit(2進法)で表現されている。
  • 例えばint型の変数は32bitで表せる範囲の整数を表現できる。

よく使う型

  • 以下によく使う型の詳細を示す。
  • 前回やらなかった型も記述しているので、順に説明する。
  • 他にも色々あるので自分で調べてみてね
型の名前 型の性質 型の値の幅(目安)
int 32bit整数
long long 64bit整数
unsigned long long 符号なし64bit整数
double 64bit実数 後述
bool 2値論理 false(0),true(1)
-2.14*10^9 \sim 2.14*10^9
-9.22*10^{18} \sim 9.22*10^{18}
0 \sim 1.84*10^{19}

浮動小数点

  • double型などの小数を表す型は浮動小数点という方式を取っている。
  • 数桁の値部分指数部分分けて保持することで計算に少しの誤差が出る代わりに広い値を持つことができる。
  • 有効数字計算を思い浮かべると理解しやすい
    • double型の場合、10進法で有効数字を15桁くらいと、指数部分の値(-308~308)の2部分でデータを保持している。
      •  
\pm2.225074*10^{-308} \sim \pm1.797693*10^{308}

double型の出力

  • double型のデフォルトの標準出力では、全部で6桁しか出力されない。
  • 出力していないだけでもっと多くの桁を保持しているはずなのでもっと出力したい
  • そこでこれを調節する方法を教えます。
//int main(){
    cout << fixed << setprecision(10);
  • 上記のコードをmainのすぐ下に書くと、全ての出力で小数部分が10桁で出力されるようになります。(コピペしてね)
  • 競プロで小数を出力するときは常に書いておくと良さそう。

型変換

  • 精度の低い型(int)と精度の高い型(long long, double)で演算すると、その結果精度の高い型で返ってくる。
  • 変数を、異なる型の変数に代入すると、その値に暗黙の型変換が行われる。

精度:int < long long < double

暗黙の型変換の注意事項

  • 暗黙の型変換を行うと、一部の情報が欠落する場合もあるので注意
    • long long の大きな値(int最大値超え)の変数をintの変数に代入すると値がバグる
    • doubleの変数をintの変数に代入すると小数部分が破棄される。

暗黙の型変換の注意事項2

  • 暗黙の型変換を過信するとコードをバグらせやすくなる。(以下、a,bはint型の変数)
    • double d = a / b;(a/bは小数部分切り捨ての整数)
    • long long l = a * b;(a*b>(intの最大値)の時は、正常に計算もできない。)
  • これらの場合は、明示的に片方を型変換すれば良い。

型変換の明示

  • 暗黙の型変換はある程度覚えて使うと良いが、明示して型変換もできる。
  • この明示的な型変換キャストという。
  • int型変数aをlong longにキャストするときは(long long)aまたはlong long(a)とすれば良い
#include<bits/stdc++.h>
using namespace std;
int main(){
    int a,b;
    cin >> a >> b;
    //long long l = a + b;では正常に計算できないかも
    long long l = (long long)a + b;
    //double d = a / b;では小数部分が切り捨てられる
    double d = (double)a / b;
    cout << l << " " << d << endl;
}

warningを消す

  • キャストしないと実行後にwarningが出る場合がある。
    • 主にunsignedsigned比較をする時
    • 次にやるvectorとstringの.size()unsigned long long型なので、比較するとコードは動くがwarningがでる
  • 鬱陶しいならキャストして消そう
#include<bits/stdc++.h>
using namespace std;
int main(){
    vector<int> vec(10);                   //次やります
    for(int i=0;i<(int)vec.size();++i){    //キャスト!!!!!!
        cin >> vec.at(i);                  //次やります
    }    
}

配列

例題

  • N人の生徒がテストを受けました。
  • 人数Nとそれぞれのテストの得点    が与えられ、全て整数とする。
  • その合計点を答えなさい。
  •      
A_i
1\leq N\leq1000,0\leq A_i\leq100

考察:例題

  • N人の生徒がテストの得点から、合計点を求めたい。
  • この時、N個変数を用意しておいて、それぞれに入力してから合計をとりたい。
  • しかし、最大でN=1000なので、そのためには最大1000個の変数が必要になり、
  • そんなときに使えるのが、配列

配列とは

  • 配列は複数の変数をまとめて入れておける便利なもので、好きな場所の値をいつでも取り出したり更新したりできる。
  • 特に1つのラベルを貼れる複数の変数配列として持っておくと便利。
  • 今回の場合は、それぞれの生徒のテストの得点を配列に記録すれば良い。

配列vector

  • vector<(要素の型)>を配列の型とし、変数同様に宣言する。この場合は配列長は0
  • 宣言時に、配列名("初期サイズ")とすると、配列長の初期設定ができる。
  • 同じく宣言時に、配列名("初期サイズ","値")とすると、中身全てを同じ"値"で初期化する。
vector<int> vec;        //通常の宣言。サイズ0の配列
vector<int> vec2(5);    //サイズ5の配列を宣言
vector<int> vec3(5,0);  //サイズ5で中身が全て0の配列を宣言

vector:

  • 配列名vecとすると、i番目にはvec.at(i)とするとアクセスできる。
  • 0番目から始まるので注意(0-indexed)
  • vec.at(i)は普通の変数と同様に扱える。
  • 入力する時は、下記のようにfor文を使うと簡潔に書ける。
vector<int> vec(5);       //宣言
for(int i=0;i<5;++i){     //入力
    cin >> vec.at(i);
}
vec.at(3) = vec.at(0)+vec.at(1);  //3番目の値に0番目と1番目の値の和を入れる
for(int i=0;i<5;++i){
    cout << vec.at(i) << endl;    //vecの中身を出力する。
}

注意事項

  • 配列の長さを超える場所は参照できず、エラーが発生する。(配列外参照)
  • 例えば、配列長5の配列を考える。
    • 0番目から4番目まである。
    • 5番目は存在しない!!
vector<int> vec(5);     //宣言
cin >> vec.at(11);      //エラー
cin >> vec.at(5);       //エラー
cin >> vec.at(4);       //OK

例題のコード例

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;      //人数
    cin >> n;   //人数nの入力
    vector<int> a(n);      //テストの得点の配列a
    for(int i=0;i<n;++i){  //入力
        cin >> a.at(i);
    }
    int ans = 0;           //求める合計点
    for(int i=0;i<n;++i){
        ans += a.at(i);    //テストの得点を全て合計点に足し合わせる。
    }
    cout << ans << endl;   //答えの出力
}

配列vector

  • vectorは配列の末尾を増やしたり減らしたりできる。
  • vec.push_back(x);
    • vecの末尾に値xの要素を追加することができる。
  • vec.pop_back();
    • vecの末尾の要素削除
vector<int> vec(5);       //長さ5で宣言
for(int i=0;i<5;++i){     //これにより、vecの各要素に入力。
    cin >> vec.at(i);
}                   //以下0-indexedで番目を表記
int x = 3;
vec.push_back(x);   //5番目の要素にx=3を追加
vec.pop_back();     //末尾の要素を削除

配列vector

  • このpush_backを用いて入力することもできる。
  • また、vec.size()とすることで、サイズを所得できる。
vector<int> vec;            //サイズ0で宣言
cout << vec.size() << endl; //この段階ではサイズ0
for(int i=0;i<10;++i){  //10回末尾に値を付け足す。
    int tmp;            //入力用変数
    cin >> tmp;         //入力
    vec.push_back(tmp); //push_backで入力したものを付け足す。
}
cout << vec.size() << endl; //サイズ、つまり10を出力。

配列vector

  • 自分で一気に代入,初期化することもできる。
  • {}の中に","で区切って値を入れる。
  • この{1,3,5}みたいなやつのことを初期化子リストと呼ぶ。
  • 初期化子リストを使うと元のvectorのサイズを無視して代入できる。
vector<int> vec = {500,100,50,10,5,1};  //初期化
vec = {1,2};        //代入

vectorの[]

  • 要素にアクセスするとき、vec.at(i)の代わりにvec[i]と書くこともできる。
  • 配列外を参照してもvec[i]はエラーを出さないので、注意が必要。(論理エラー)
  • vec[i]の方が、vec.at(i)に比べて実行速度が若干速い。(あとタイプしやすい)
  • 慣れるまではatを使う方が良いと思う。
vector<int> vec = {0,1,2,3,4};
//vec.at(0) = 1;  
vec[0] = 1;     //内容はatと全く同じ
cout << vec[3] << endl; //入出力もできる。
cout << vec[5] << endl; //配列外だが、ただ未知の値が出力されるだけ

二次元配列

二次元配列とは

  • 2つの場所を示す値によって、定められる配列のこと。
  • 要は「縦と横からそれぞれ何番目にあるか」によって値を格納する表のようなもの。

実際の作り方

  • vectorを入れ子にすることで表現する
  • 初期化の表現がやや複雑なので注意
vector<vector<int>> vec; //宣言
vector<vector<int>> vec2(5); //サイズの初期化(縦のみ)
vector<vector<int>> vec3(5,vector<int>(5))   //サイズの初期化(縦横)
vector<vector<int>> vec4(5,vector<int>(5,0)) //サイズ、中身の初期化

vector<vector<int>>のイメージ

vvec.at(0)

vvec.at(1)

vvec.at(2)

vector<vector<int>> vvec

  • このように、vector<int>の配列を持っているというイメージ。
  • これを(右の)二次元の表と看做している。
  • この図の向きに行と列を設定することが多い

vector<vector<int>>の使い方

  • vector<vector<int>> vvec;と宣言したとする
    • vvec.at(i).at(j)によって、i番目のvector<int>のj番目の要素にアクセスできる。
      • これをi行j列目の要素だとする​​
    • vvec.size()によって、縦のサイズ(行数)を所得できる。
    • vvec.at(i).size()によってi番目のvectorのサイズを所得できる。(列数)

2次元配列コード例

2次元vectorの入力を受け取る方法。

#include<bits/stdc++.h>
using namespace std;
int main(){
    //5x10で中身が全て0の2次元配列を宣言。
    vector<vector<int>> table(5,vector<int>(10,0));
    //2重ループで入力を受け取る。
    for(int i=0;i<(int)table.size();++i){
        for(int j=0;j<(int)table.at(i).size();++j){
            cin >> table.at(i).at(j);
        }
    }
}

文字列string

文字型char

  • 文字を扱うときはchar型を使う。
  • char型は後述するstring型とは違い、半角英数字記号1文字までしか保持できない。
  • 代入するときは、' 'で囲ったものを代入する。("と'は異なるので注意)
char c1,c2;                       //宣言
cin >> c1;                        //入力
c2 = 'a';                         //代入
cout << c1 << " " << c2 << endl;  //出力

文字型char

  • char型は内部的には0~255の短い整数を持っている。
  • 各整数に対し、ASCIIコードというルールを用いて、各文字を割り当てている。
    • 気になる人はASCIIコード表で検索
  • 'a'-'z'、'0'-'9'、'A'-'Z'のそれぞれは連続してASCIIコードに配置されている。
  • 入力の時は(空白は飛ばして)1文字ずつ読み込む。
char c1,c2;                //宣言
cin >> c1;                 //入力
c2 = 'a';                  //代入
cout << c1 << c2 << endl;  //出力

char型の比較演算

  • char型の比較演算では、内部の整数の比較をしている。
  • 'a'-'z'、'0'-'9'、'A'-'Z'のそれぞれは連続していることから、'a'<'b','3'>'2','A'<='D'のそれぞれが真。
    • 小文字の中だけ、あるいは大文字の中だけなら辞書順
    • 同じく数字の中なら、直感的な比較が可能

char型の算術演算

  • char型の内部の整数を計算するので、今までの全ての算術演算が適応可能
    • 恐らく使うのは足し算と引き算だけ
  • 同じ系統の文字の中なら、連続しているので、その文字の前後の文字を作れる。
    • 'b'=='a'+1, '2'=='4'-2, 'D'=='A'+3などが真

char型の算術演算

  • 同じ系統の文字の中なら、連続しているので、その文字の前後の文字を作れる。
  • うまくやると、小文字から大文字を作れる。
    • 例えば、'A'-'a'を足せば良い。
      • 小文字も大文字も連続しているので、その距離を足せば大文字にできる。
      • もちろん逆もできる。
char c = 'd';
c += 'A'-'a';      //大文字になる
cout << c << endl; //つまり'D'を出力
c += 'a'-'A';      //小文字になる
cout << c << endl; //つまり'd'を出力

文字列型string

  • プログラミングで文字列(複数の文字の集まり)を扱いたい時、string型(文字列型)を使う。
  • 代入するときは、" "で囲ったものを代入する("と'は異なるので注意)
string str1,str2;    //宣言
cin >> str1;          //文字列の入力
str2 = "hello";       //文字列の代入
cout << str1 << endl; //文字列の出力

シングルクオートとダブルクオート

  • 'char型なので1文字
  • "string型なので何文字でも良い
  • 'a'string型には代入できない要注意!!!

文字列型string

  • 以下string型変数名をstrとすると
  • 左からi番目の文字を取り出したいときは、vectorの時と同様に、str.at(i)で取り出せる。(0番目からスタート,範囲外アクセスはエラー)
  • 文字列の長さを取り出したいときは、これもvector同様にstr.size()で取り出せる。
string str;    //宣言
str = "hello";       //文字列の代入
cout << str << endl; //文字列の出力
cout << str.size() << endl;  //文字列の長さを出力。今回は5。

stringとchar

  • str.at(i)によって取り出される文字はchar型
  • str.at(i)には'  '(シングルクオート)でのみ代入できる。
string str;        //宣言
char c;            //宣言
str = "hello";     //代入
c = str.at(0);     //c='h';と等価
str.at(1) = c;     //str=="hhllo"に
str.at(2) = 'h';   //str.at(2)はchar型なので代入は''
cout << c << endl; //'h'を出力
cout << str << endl; //"hhhlo"を出力

string型の大小

  • 比較演算子でstring型を比較するとどうなるのか
    • 基本的には辞書順
      • ("ab"<"ac","a"<"aa","AB"<"AC"は真)
    • ただし、大文字小文字混ざってるとダメ
    • 全く同じ文字列の時だけ==になる

string型の'+'演算

  • string型には'=','+','+='の演算子と比較演算のみが定義されている。(他の演算子は使えない)
  • '+'を用いることで、文字列の結合ができる。
  • '+='は文字列の語尾に文字を付け足せる
string str1,str2;
str1 = "Hello,";
str2 = "world!";
string str3 = str1+str2; //"Hello,world!"になる
//str1 = str1+str2と同じ
str1 += str2; //"Hello,world!"になる

まとめ:string型

  • string型は+と比較がかなり優秀なので、押さえておくと良い。
  • string型におけるcinも競プロでは使いやすく、スペースか改行までを自動で入力してくれて便利。

関数

関数とは?

  • 値(引数と呼ぶ)を渡すと、何かの処理をして、結果を返してくれる(返り値と呼ぶ)もの。
int a,b;
cin >> a >> b;
//min()は2値を比較して小さい方を返す。
int c = min(a,b);
  • 1つのプログラムで同じ処理を複数回実行したい時に便利。

関数の使い方

  • 関数は引数の個数を指定し、返り値を持っており、引数に対する演算を行うことによって値を返す。
  • 関数名(引数1,引数2,...)とすることで関数を呼び出せる。
int a,b;
cin >> a >> b;
//min()は2値を比較して小さい方を返す関数。
int c = min(a,b);

STLについて

  • STLとは、C++で標準で使える関数の集まりのこと。
  • つまり、自分で実装しなくても使える便利な関数!
  • STLの関数
    • min(a,b):a,bの小さい方を返す
    • max(a,b):a,bの大きい方を返す
    • 他にも多数!(少しずつ紹介します。)
int a,b;
cin >> a >> b;
//min()は2値を比較して小さい方を返す関数。
cout << min(a,b) << endl;
//max()は2値を比較して大きい方を返す関数。
cout << max(a,b) << endl;

min,max関数の注意

  • min,max引数の型を自動で判別してくれるので、基本的に型について気にする必要はない
  • しかし、2つの引数の型が異なる場合において、エラーがでる。
  • そんな時は、キャストして、返り値にしたい方の型に揃えよう
int a;
long long b;
cin >> a >> b;
//min()は2値を比較して小さい方を返す関数。
//cout << min(a,b) << endl;これはエラー
//以下のように、型を揃えてあげよう。
cout << min((long long)a,b) << endl;

min,max関数(3値以上)

  • min,maxで3値以上を比較しようとするとき、min()の()に3値以上をそのまま書くエラーが出る。
  • そんな時は、初期化子リストで{a,b,c,..}とすると、比較できる。
int a,b,c,d;
cin >> a >> b >> c >> d;
//min()は{}内の最小を返す関数でもある。
//cout << min(a,b,c,d) << endl;これはエラー
//以下のように、さらに{}で閉じよう。
cout << min({a,b,c,d}) << endl;
cout << max({a,b,c,d}) << endl;

min,max関数(配列)

  • min,maxで{}を用いても、書ける数はたかが知れてる。
  • 特に、配列の最小値,最大値を出したい場合はfor文を書こう。
//要素nの配列の最小値を出力する。
int n;
cin >> n;
vector<int> vec(n);
for(int i=0;i<n;++i){ //入力
    cin >> vec.at(i);
}
int res = vec.at(0);  //初期値
for(int i=1;i<n;++i){ //計算
    res = min(res,vec.at(i));
}
cout << res << endl;

vector周りのSTL

  • sort(vec.begin(),vec.end());
    • これによって低い方から順番に並べることができる。
  • reverse(vec.begin(),vec.end());
    • これによって順番を逆にすることができる。
vector<int> vec = {3,1,4,5,9,2};
sort(vec.begin(),vec.end()); //低い順に並べなおす。
                             //{1,2,3,4,5,9}になる。
reverse(vec.begin(),vec.end()); //順番を逆にする。
                                //{9,5,4,3,2,1}になる。

char周りのSTL

  • char chとする。
  • isupper(ch)/islower(ch)/isdigit(ch)はそれぞれ大文字/小文字/数字ならば真を返す。
  • toupper(ch)/tolower(ch)はそれぞれchを大文字/小文字にしたものを返す。
    • 大文字/小文字でなければそのまま返す。
string str = "Hello,world!";
//小文字を数える。
int low = 0;
for(int i=0;i<(int)str.size();++i){
    if(islower(str.at(i))) ++low;
}  //low=9
//全て大文字にする。
for(int i=0;i<(int)str.size();++i){
    str[i] = toupper(str.at(i));
}  //str="HELLO,WORLD!"

関数の実装について

  • 関数を作る(定義するという)時に指定するもの
    • 返り値の型
    • 引数の個数とそれぞれの型
    • 処理内容
  • 返り値の中身はreturn "値";で返す。
/*
[返り値の型] [関数名]([引数1の型] [引数名1],[引数2の型] [引数名2],..){
    [処理内容]
}
*/
int min(int a,int b){
  if(a < b){
    return a;
  }else{
    return b;
  }
}

関数の実装について弐

  • 関数の引数は基本的にコピーして、関数に渡される。
  • そのため、関数内で引数を弄っても、元の引数となった変数は変化しない。
  • 変えたい時は、関数定義の引数に&(変数)と書くと良い。
//この関数では元の引数の値は書き換えられない。
//引数がコピーされてaに渡されている。
int get_minor(int a){
    a *= -1; //勿論コピーされたaの計算は可能
    return a;
}
//この関数で引数の値を書き換えられる。
int change_minor(int &a){
    a *= -1; //元のaも-1倍される。
    return a;
}

例 min関数の実装(int型)

#include "bits/stdc++.h"
using namespace std;
//int型のmin関数と同等なint_min関数
int int_min(int a,int b){
  if(a < b){
    return a;
  }else{
    return b;
  }
}
int main(){
  int a,b;
  cin >> a >> b;
  //a,bの小さい方を出力
  cout << int_min(a,b) << endl;
}

関数の型について

  • 関数は必ずしも値を返さなくても良い
  • その時はvoid型で定義。
  • void型ではreturnしなくても良い。returnすると、その段階で関数が終了する。
  • また、引数がない時は()に何も書く必要なし
#include <bits/stdc++.h>
using namespace std;
//"Hello!"を出力する関数
void hello(){
    cout << "Hello!" << endl;  
    return; //あってもなくても良い
}
int main(){
    //画面に"Hello!"を出力
    hello();  
}

再帰関数について

  • 関数の中で自分を呼び出す関数のことを再帰関数と呼ぶ。
  • 分岐させながら処理を行う時に便利
  • あまりに深く再帰しすぎるとエラー(スタックオーバーフロー)を吐くのが玉に瑕。
  • 必ず最後には終わるようにすること。
#include <bits/stdc++.h>
using namespace std;
//x回"Hello!"を出力する関数
void hello(int x){
    if(x<=0) return;
    cout << "Hello!" << endl; 
    hello(x-1); 
}
int main(){
    //画面に10回"Hello!"を出力
    hello(10);  
}

おまけ:main関数について

  • 気づいてる人もいると思うが、mainも関数
  • main関数最初に自動で呼び出されることが決まっている関数。
  • int型の返り値を持っていないように見えるが、実はコンパイル時にreturn 0;が追加される。(0は正常終了の意,他は以上終了)
  • このreturn 0;は自分で書いても良い。
#include "bits/stdc++.h"
using namespace std;
int main(){
    cout << "Hello, world!" << endl;
    return 0; //書いてもOK!
}

おまけ:includeについて

  • STLの関数などを使うにはincludeライブラリを読み込む必要がある。
  • 例えば、cin/coutを使うには<iostream>をincludeする必要がある。
  • <bits/stdc++.h>全てのライブラリを含むライブラリで競プロにはとても便利!
//#include <iostream>としても良いが、
//以下のようにすると色々気にせずに済んで楽
#include<bits/stdc++.h>
using namespace std;
int main(){
    cout << "Hello, world!" << endl;
}

おまけ:using namespace~について

  • STLの関数の多くは関数名の重複を避けるため、std::minなどとしないと呼べない。
  • しかし、競プロではどうでもいい
  • そこで、using namespace std;とすることによって、std::を外すことができる。便利。
#include<bits/stdc++.h>
//using namespace std;をつけないと、以下のように書く羽目に
int main(){
    int a,b;
    std::cin >> a >> b;
    std::cout << std::min(a,b) << std::endl;
}

まとめ

  • おまけは、おまじないを消去したかっただけなので、気にする必要はないと思う。
  • それよりも、きちんとSTLを使いこなせるようになると、行数も減らせてバグも減るので、ちゃんと押さえておくこと。

計算量

計算量とは

コンピューター上でプログラムを実行するのに必要な資源

2種類の計算量

  • 計算量には2種類ある
    • 時間計算量:計算する際の基本演算数
    • 空間計算量:計算するのに必要なメモリ
  • 実行時間はコンピューターのスペックに依存するので、演算数で定義
  • 単に計算量と呼ぶときは時間計算量を指すことが多い(空間計算量が問題になることは比較的少ないので)

実行時間制限

  • 競プロでは実行時間制限がある(他方でメモリの使用制限は余裕があることが多い)
  • そのため、実行時間をきちんと見積もらなければならない。
    • 入力サイズが小さい
      • 遅くても単純で間違いにくい計算手法(以下アルゴリズムと呼ぶ)が使える。
    • 入力サイズが大きい
      • 難解でも高速なアルゴリズムを考える必要がある。

実行時間の見積もり方

  • 要は、それぞれの計算手法ごとに時間計算量を見積もれれば良い。
  • もっと言えば、入力サイズnに対してどの程度の時間がかかるかを見積もりたい。
  • そこで登場するのがランダウの記号

ランダウの記号の理念

  •      の時の関数の大まかな振る舞いを見る時に用いる記号のこと
  • 例えば、                  と は     において(次数が上がったものに比べると)それほど大きな差は出ない。
    •                               のとき、
      •                     
      •  
  • この事実から、演算数をnを用いて表したものの次数に着目すれば良いことがわかる。
n\rightarrow\infty
3n^2+n+5
n^2
n\rightarrow\infty
n=100000(={10}^5)
3n^2+n+5:30,000,100,005
n^2\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ :10,000,000,000(={10}^{10})
n^3\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ :1,000,000,000,000,000(={10}^{15})

ランダウの記号とは

  •     と同じように変化する関数の集合を    と表現する。
  • また、通例          に属することを=で表す
    •  
    •  
    •  
  • 要は低次項を消し、最大次項の係数を1にしたものを    の中に入れれば良い。
  • 競プロでは    のことをオーダー と呼ぶことが多い
3n^2+n+5=O(n^2)
n^2=O(n^2)
n^3=O(n^3)
O(n^2)
n^2
O(n^2)
O(\ \ )
O(n^2)
n^2

計算量の見積り

  • オーダーを見積もることで妥当性のある計算量を見積もれることは説明した。
  • ある問題の制約条件が                                で、今のアルゴリズムのオーダーが     であれば、計算量の概算値の最大は        であると言える。
  • 計算量を見積もる時はこのように原則、最悪のケースを想定してオーダー計算する。
n≤100000(={10}^5)
O(n^2)
{10}^{10}

計算量の目安

Q:計算量の見積りは立てられるけれど、実際どれくらいから厳しいの?

A:以下を参考にしよう。

{10}^6
{10}^7
{10}^8
間に合う
おそらく間に合う
少し厳しい

参考:蟻本p.20

ということは

  • ある問題の制約条件が                                で、今のアルゴリズムのオーダーが     であれば、計算量の概算値は        であると言える。
    • このオーダーではまずい。
    • より効率の良いアルゴリズムを探す必要がある。
n≤100000(={10}^5)
O(n^2)
{10}^{10}

例題

整数Nが与えられる。

1からNまでの総和を計算しなさい。

1\leqq N\leqq{10}^9

問題文

制約

実行時間制限2sec

入力例

3

出力例

6

解法1 愚直解

  • 普通に1からNまで足し合わせる。
  •  
O(N)
#include<bits/stdc++.h>
using namespace std;
int main(){
    int N;
    cin >> N;
    long long ans = 0;
    for(int i=1;i<=N;++i){
        ans += i;
    }
    cout << ans << endl;
}
  • 最悪計算量     で恐らく間に合わない。
10^9

ところで

等差数列の和の公式って知ってますか?

例えば1からnまで順に足し合わせるとき、

1 2 3 4 .... n-3 n-2 n-1 n
n n-1 n-2 n-3 ... 4 3 2 1
n+1 n+1 n+1 n+1 ... n+1 n+1 n+1 n+1

合計

  • 元の数列を逆順に並べたものを元の数列に足し合わせると合計値が一定になる。
  • 上は今回の場合の例
  • よって、全ての和は(n+1)*n/2で計算できる。

解法2 等差数列の和の公式

  • 1からNまでの和はN*(N+1)/2
  • これを答えとすれば良い。
  •  
O(1)
#include<bits/stdc++.h>
using namespace std;
int main(){
    int N;
    cin >> N;
    long long ans = N*(N+1)/2;
    cout << ans << endl;
}
  • これなら余裕で間に合う。

計算量を落とす

  • 今回の問題では、計算量を         から  まで落とした。
    • 解法を考える。
    • 制約に沿って計算量を概算
      • 間に合うなら書いて提出
      • 間に合わないなら再考してより良いアルゴリズムを考えよう。
  • 上記手順に沿って今後はプログラムを書くことになる。
O(1)
O(N)

STLの計算量

  • sort(vec.begin(),vec.end())
    • O(nlogn)
    • 詳しい理由は第3回でお話しします。
  • reverse(vec.begin(),vec.end())
    • O(n)
    • 後ろから1つずつ前に再代入するため。
  • 今までやった他のSTLは大体O(1)です。
    • 自分で実装してみると、ifひとつくらいで書ける。

貪欲法

アルゴリズムとは

  • アルゴリズムは、問題解決の手法のこと。
  • 今からやる貪欲法もアルゴリズムの1つ。
  • 競プロでは、どのようなアルゴリズムで問題を解くかが重要となる。
    • 良いアルゴリズムは計算量が小さい

貪欲法とは

  • 貪欲法は後のことを考えずに、各段階で次の状態の値を最適にすることを選び続けるアルゴリズム。
  • 必ずしも、上手くいくとは限らないが、有用な解き方の1つである。

例題0:カードの選択

  • 10枚のカードに番号 が書かれている。
  • この中から3枚のカードを選んで、書かれた番号の合計値を最大化し、出力しなさい。
  •  
a_i
1\leqq a_i \leqq1000

解法:カードの選択

  • 1枚ずつ選ぶとする。
  • 各段階で最も大きいものを選べば良い。
#include<bits/stdc++.h>
using namespace std;
int main(){    //これは常に3つ最も大きいものを持っておくコード
    int mx1=0, mx2=0, mx3=0; //それぞれ左から最も大きいものを順に入れる。
    for(int i=0;i<10;++i){
        int a;
        cin >> a;
        if(a>mx1){ //今持ってる3つよりも大きい時、ずらして更新する
            mx3 = mx2;
            mx2 = mx1;
            mx1 = a;
        }else if(a>mx2){ //二番目よりも大きい時、ずらして2つ更新
            mx3 = mx2;
            mx2 = a;
        }else if(a>mx3){ //三番目よりも大きい時、それだけを更新
            mx3 = a;
        }
    }
    cout << mx1+mx2+mx3 << endl; //答えを出力
}

解法:カードの選択

  • さっきの解法より、計算量が若干増えるが、書くならこれの方が手っ取り早い。
  •  
#include<bits/stdc++.h>
using namespace std;
int main(){ 
    vector<int> a(10); //カードに書かれた番号を持つ配列
    for(int i=0;i<10;++i){
        cin >> a.at(i);      //入力
    }
    sort(a.begin(),a.end()); //小さい順にソート
    cout << a.at(7)+a.at(8)+a.at(9) << endl; //答えを出力
}
O(nlogn)

例題1:硬貨の支払い1

  • N円を硬貨で支払う。
  • 硬貨は1円,5円,10円,50円,100円,500円。
  • 出来るだけ少ない枚数で支払いたい。
  •  
0\leq N\leq10^9

解法:例題1

  • 略証:各硬貨はそれより小さい硬貨を複数枚積むと、ちょうど同じ価値になる。
  • よって、出来るだけ大きい硬貨から順番に払っていけば良い(貪欲法)。
  • ex.) 768=500x1+100x2+50x1+10x1+5x1+1x3

貪欲法について

  • 反例が見つからず、直観的に大丈夫と感じたなら、試してみるといいかも。
  • 貪欲法で解けることを証明(略証)できるとなお良い
  • 証明するのが難しいこともあるので、取り敢えずは反例探しを怠らないようにしよう。

例題2:硬貨の支払い2

  • N円を硬貨で支払う。
  • 硬貨は1円,5円,8円,10円,50円,100円,500円。
  • 出来るだけ少ない枚数で支払いたい。
  •  
0\leq N\leq50,000

考察:例題2

  • さっきと同じように貪欲法?
  • 実は例外(うまくいかないケース)が存在。
  • (例外) N = 16
    • 16 = 10x1 + 5x1 + 1x1 で3枚(貪欲法)
    • 16 = 8x2 で2枚(こっちが最適)
  • この問題は第5回でやる動的計画法によって解くことができます。
  • (今日はやりません)

最後に

このスライドについて

  • KMCに入部済みの人なら、内部Wikiからアクセスできるので、自由に見てね
  • プログラミング基礎の部分は、時間の都合で、かなり端折って作った上に、簡潔にする為にいくつか方便を使って説明しています。
  • おそらく初心者の人もこれで最低限の知識は身についたはずなので、プログラミング入門サイトが読みやすくなったのでは?

次回以降のこと

  • 次回以降はプログラミングそのものについてはあまり触れないので、少しでも勉強してきてくれると理解しやすいと思います。
  • アルゴリズムについて基本的なところからやっていこうと思ってます。

オススメの勉強法

  • AtCoderというサイトが日本語で競プロができるので、挑戦してみよう。
  • まずは、AtCoderのAtCoder Programming Guide for Beginners (APG4b)が、初心者にオススメなので時間あるときに見るといいと思う。
  • それが終わったら、AtCoder Beginners Selection を上から解いていこう。

今からのこと

  • これからプログラミングのコンテストを立てるので、参加しましょう。
  • コンテスト中に分からないことがあれば、ググったり、自分や近くの先輩方に聞くなりして頂いて大丈夫です。
  • 特に今日初めてプログラミングする人は、先輩方に色々手伝ってもらうといいと思います。

開発環境

  • 持ってない人は今日のところはWandboxを使おう。
    • 実行もできるので、使い勝手はかなりいいと思います。
  • 作りたい人は前回(第0回)のスライドを見るか(部員のみ)、コンテスト終了後にでも先輩方に聞いてみるといいのでは。(僕はMacしか分からないです)

参考文献

  • プログラミングコンテストチャレンジブック第二版
    • 通称 蟻本。
    • アルゴリズムのあれこれが載ってます。
    • 競プロerのほとんどが持っている。
    • 競プロやるなら、買って損はないと思います。
  • AtCoder Programming Guide for Beginners(APG4b)
    • プログラミングの基本部分についてはかなり参考にしています。
    • こっちの方がその部分についてはかなり詳しいので、初心者なら一読の価値あり。

第1回:プログラミング基礎(続),計算量,貪欲法

By procon2019

第1回:プログラミング基礎(続),計算量,貪欲法

発表日時 2019年4月12日(金) 18:30-21:00

  • 851