競プロNormal第2回

担当: kidman7

自己紹介

理学部2回生 kidman7

  • Python, C++を書いてます。
  • ポケモンとか動画編集とか最近ハマってます
  • (実は最近競プロできてないとか言えない...)
  • 家のWiFiが繋がらなくなったり、家の鍵が壊れたり最近運がとても悪いです
  • どなたか電子ピアノを私に恵んでください。

型について

bitについて

パソコンでは、bit列(0と1のデータ集合)で

データが保存されています。

 

32bitは0と1の数が32個あるということですね。

 

ちなみに、32bitあれば\( 2^{32} \simeq 10^{9} \)種類くらいは識別できます!

データ型の種類

前回(みんないたのでしょうか?)で使ってたのは

  • int
  • double
  • string

などでしょうか??

 

これらは、データ型といって

「今からどのような値をいれますよ」

という宣言になっています。さて、どのような値がこれらに入れられるのでしょうか?

数値を入れる型たち

種類 範囲
int 整数値(32bit)
long long 整数値(64bit)
float 小数(浮動小数点数)...(32bit)    後述
double 小数(浮動小数点数)...(64bit)
unsigned 符号なし整数...(64bit)
bool 真偽値(1bit) false(0)とtrue(1)

\(\pm 2.14 \times 10^9\)

\(\pm 9.22 \times 10^{18} \)

\(0 〜  1.84 \times 10^{19} \)

これらはよく使うので覚えといてください!!

(floatとunsignedは滅多に使わないけど...)

浮動小数点とは...

32bit(float)とすると

\(-2.4545 \times 10^{2} \)

を表したいとすると

 

符号部(-)で1bit

指数部(2)を8bitで表して

2.4545を23bitで表す。

 

という感じ

精度は落ちるけど、データ量が大きい!

doubleの上限は\(\pm 1.8 \times 10^{308} \) !

競プロで浮動小数点を出力する時

double x = 1.233444444442123;
cout << x << endl;

出力される答えは 1.233444

データとしては保持できてるのに、出力されない!!

double x = 1.234567890;

cout << fixed << setprecision(4);
cout << x << endl;

出力される結果は1.234567890

set.precisionのおまじないを書きましょう!

(printf("%.9lf",x)とかでもいいですけど...)

型変換のお話

intからdoubleにしたい!!

逆も然り。

 

さあどうする。

二通りありまして

「暗黙的型変換」と「キャスト」があります。

キャストとは

明示的に(宣言して)型を変換することです。

例えば

vector<int> vec(10);

for(int  i=0; i < (int)vec.size(); i++)

このような感じで、なにかのあたいの前に(int)と置いています。

 

これでint型に変換しています。

(vec.size()の方はunsigned long long型で、キャストしないとwarningでるのでおすすめ)

暗黙的型変換とは

double x  = 1.2;
int y = 3;

double z = x + y;

こんな計算をすると、自動的にyはdoubleとして計算されます。

(つまり違う型同士の演算でもエラーしない)

注意

割り算!!

 

int a=3;  int b=4;  double c = 5.0;

a/b...これはint型(0が出力される)

c/b...これはdouble型

こんな感じで精度が高い方に合わさるようになります。

(int < long long < double)

整数同士の割り算は(a+0.0)/bや

(double)a/bなどして気をつけましょう!

Pythonなら....(ry

配列

前回、for文を学んだかと思います。

#include <iostream>

using namespace std;

int main(){
	for(int i=0 ; i < 10 ; i++) {
		cout << i << endl;
 	}
}

これと組み合わせて、連続的なデータを

簡単に処理する方法を学びましょう!

問題

ここに各都道府県のC-ウイルスにかかった人のリストを与えます。感染者の平均を出しなさい

(コロナウイルスとは何の関係もございません)

 

\(n = 10\)

1 ,23, 145, 19, 17, 1, 4, 90, 9, 2

nが小さいから、電卓でも暗算でも計算できそう。

 

でもnが100000とかになったら...?

そこで配列の出番です!

配列とは

  • 値が順番に並んでいて
  • 値を取り出すのに時間がかからない
  • 自由に値を変更可能!!

 

というプログラムを書くにあたって、

一生のお友達になるであろうお方でございます!

 

今回はその中の書き方の一例を示しておきます!

Vector

vector配列は、収納ケースみたいなものです

vector<int> vec = {1,2,3,4}

<>の中身は先ほど紹介した型です。

こんかいはintを指定しているので、整数を保持させています。

一つずつアクセスする(値を持ってくる)ためには

vector<int> vec = {1,2,3,4}
cout << vec.at(0) << endl;
vector<int> vec = {1,2,3,4}
cout << vec[0]<< endl;

と書きます。(右の方が使うかな)

Vector

vector<int> vec(要素の数)とすると、int型の器を欲しい分だけ作ってくれます。先ほどのアクセスのやり方を含めて

問題の回答をしたに表示しますね。

vec.size()はvectorの要素の数です!

#include <iostream>
using namespace std;

int main(){
    int n; cin >>  n;
    vector<int> vec(n);
    for(int i=0  ; i<n ;  i++){
    //入力をします
    	cin >> vec[i];
    }
    for(int i=0  ; i<(int)vec.size() ;  i++){
    //合計を出します
    	sum += vec[i];
    }
    cout  << (sum+0.0)/n << endl;
}

vecって名前は使ってますが、変数名なので

vでもveでもいいですよ!(vectorはだめかな)

vector<int> vec(5,0)とすると、

5個の値を0で初期化してくれます

vec.assignとかpush_backとかemplace_backとかいろいろありますが、

それはまた後のお話

注意する点

  • 一番最初は「0番目」であること!!!!(0-index)

  • 配列外参照をすること

vector<int> v(10,0);
cout << v.at(10)  << endl;

これはエラー。0から始まるので、10個要素をとってきたら
最後のindexは9!!!

cout  << v[10] << endl;
これは未知の値を返す。
vectorだとわりかし0を返してくる

始めたばかりはよくやらかします。

後から出てくるdpで私もいまだにやらかします

二次元配列

vectorで二次元配列

vectorではデータ型のところにさらにもう一個

vectorを入れることによって二次元配列を作ることができます。

vector<int>

vector<vector<int>>

vector<int>がデータ型だからできるんです!

実際の作り方

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)) //サイズ、中身の初期化

この表記に慣れるまでに少し時間がかかると思いますが、最初はコピペでも大丈夫ですよ!

二次元配列の構造

index 0 1 2 3 4
0 123 456 567 15 345
1 567 44 ......
2
3
4

要素のアクセスはvec[縦][横]

もしくはvec.at(縦).at(横)

 

vec[0][1] は456ですね

文字列

文字列を扱うのは2種類の型

  • string
  • char

何が違うのでしょうか???

char

これは、一文字しか代入できません。

また、文字を囲まなければいけなくて

そのもじは「' '」(シングルクオーテーション)です

ダブルクオーテーションはダメ!!

char c = 'x'
// => OK

char cc = "x"
// => NG

char ccc = 'as'
// => NG

Charのいいところ

  • 文字の比較ができる
  • 一桁の数字の計算ができる!
  • アルファベットの文字の差がわかる

 

何故???

charは番号で管理されています。

例えば、'A'は65というように。

'A'と'B'は65と66。つまり引き算したら1!

(問題)char x =  '3'としたとき,x - '1'はなーんだ?

String

  • 複数の文字を保管できる!
  • 中身はvector<char>と同じ!!

つまり配列のように扱える!

string s = "I am editing this  presentation at 5:21 a.m."
char x = s[3]

// x => m

for文と組み合わせることで、一文字一文字の処理が行える!!

関数

関数とは

引数を渡すと、返り値を戻してくれる優れもの

 

複数回処理をするときや、プログラムを見やすくするためによく使われます。

今までのat(i)も関数ですよ!

(iが引数、格納された要素が返り値)

関数は自作できるし標準装備でついているものもある!

まずは一回作ってみましょう!

int my_function(int x,int y){
	return 0
}

関数に必要なもの

名前(好きなの)

返り値の型

引数(必要な数)

返り値。最初に指定した型を返さないと

いけません

最小値を返す関数

int int_min(int x, int y){
    if (x > y){
    	return y;
    }else{
    	return x;
    }
}

int main(){
	int x = int_min(4,5);
    
    // => 4
}

こんなので完成です!

ifを使った時、return に届かない部分があるとエラー出るので注意!

実は標準装備(STL)に...

  • min
  • max
  • sort(順番を揃える)
  • reverse(逆順に)

などはすでにSTL(standard template library)があるんです

#include <bits/stdc++.h>を書いている型はすぐ使えるはず!

 

書いてない人は#include <algorithm>で。

#include  <bits/stdc++.h>
using namespace std;

int  main(){
	int a = 12;
    int b = 34;
    
    cout  << min(a,b) << endl;
    cout << max(a,b) << endl;
    cout << min({1,2,3}) << endl;
    
    vector<int> v = {1,4,5,6,2,1};
    sort(v.begin(),v.end());
    
    // => 1,1,2,4,5,6,
    
    reverse(v.begin(),v.end());
    
    // => 6,5,4,2,1,1
}

minやmax は同じ型にしてね♪

STLだと3つ以上の値も{}で括れば比較可能!

 

STLは本当に何でもあるからぜひ調べてみてください!

include

includeはSTLの読み込みを行っています。

例えば、#include <iostream>とすると、coutやcinの

関数が読み込まれます。

 

#include<bits/stdc++.h>は<iostream>とか<cmath>とか色々含んでいるのでこれ一つで大丈夫ってことですね!

 

コンパイル時間かかって嫌って人はそれぞれの読み込みでもいいですよ

using namespace std;

C++のSTLは本当は

std::cout << 12 << std::endl;

 

と書かなければなりません。(名前空間)

これは色々機能を自分で追加するときに困らなくするためですが、競プロではそんな機会はきません!

 

なのでこのおまじないを書くことでタイピングを減らしてます。もし企業さんに行ってC++を書くことになったらこれは使わないようにしましょうね!

再帰関数

再帰関数とは、関数の中に関数が入り込んでいるもの

再帰関数で1〜10までの和を求めてみましょう

int recur_num(int n){
    if(n == 1) return 1
    
    return n +  recur_num(n-1);
}

できるんですよこれが

 

あまりにも眠いので元気になったら図解説明入れます。

https://qiita.com/drken/items/23a4f604fa3f505dd5adからの拾ったものです。

 

(まさか同じ関数を作ってるとは思わなんだ)

計算量

計算量って何だ??

パソコンってすごいからどれだけ莫大な計算をしても大丈夫でしょ??

 

 

NO!

計算量とは

  • 時間計算量...どのくらい計算に時間がかかるか
  • 空間計算量...どのくらいメモリを消費するか

 

の2つが存在します。

 

例えば、先ほどの動画では時間計算量がとてつもなく大きくなりました。

 

ディープラーニングとかは空間計算量(メモリ)を大量に消費するので空間計算量が増えたりします。

 

競プロでは時間計算量がネックになってきます。

どういうことか?

問題:1から\(10^{10}\)までの合計を求めよ

(実行時間制限2秒)

////.....省略......
long long sum = 0;
for(int i = 0 ; i < 10000000000 ;  i++){
	sum += i;
}

cout << sum << endl;

簡単じゃん!!.....??????

本当に??

今、forループ何回繰り返された??

\(10^{10}\)回。

 

競プロでは基本的に\(10^{8}\)回くらいforが繰り返されると、実行時間2秒とかでは計算できなくなります。

じゃあ、どのように計算量を見積もればいいの???

ランダウの記号!!

ランダウの記号とは

かいつまんで説明すると、ある関数を無限大に飛ばした時の関数の傾向を表す...と言えばいいかな。

例えば、

\(3n^2+4n+5\)なら\(O(n^2)\)

\(8\log n + 1\)なら\(O(\log n)\)

\(e^n + n!\)なら\(O(n!)\)

3なら\(O(1)\)

 

まあ、要するに発散速度が一番高い項の定数倍を除いたものがランダウ記号の中に現れるってことですね

計算量の確認のしかた

基本的に、for文の回数で確認しましょう!!

for(int i = 0 ; i < m ; i++){
	for(int j = 0 ; j <  n  ; j++){
		//something
    }
}

外側のfor文がm回、内側のfor文がn回繰り返されているので、計算量は\(O(mn)\)

競プロではこの\(O\)の中身(この場合はmn)が\(10^8\)を超えなければ良い!

つまりうまいアルゴリズムを使おう!

さっきの問題

解説する必要もないとは思いますが、

1から順番に足した和は

\[\dfrac{1}{2} n(n+1)\]

で計算されるので、これを使えばfor文を使わないので

計算量が

\(O(n)\)から\(O(1)\)に減ったことがわかります!

このように競プロでは

愚直なアルゴリズムではダメってなったりすることが多々あるので、うまいアルゴリズムを見つけていく!!

 

沼にハマると抜けなくなりますね!

(今ハマってない人が行ってもあれだけど,,,,)

貪欲法

問題

今からN個のランダムな数字をいいます。

この中から3つ選んで合計を最大にしてください。

考えること

やっぱり大きい順にとっていけば最大になるやろー

 

 

大正解!

それが貪欲法

貪欲法とは

  • とにかく現時点で一番利得(最大、最小)が大きいようにする
  • それを繰り返して行ったときが全体でも利得が一番大きい!
  • これはうまくいく時といかない時が....

後々でてくるDP(動的計画法)につながりますが、それはだいぶ先で学びましょう

貪欲法はみんなが一番最初に考える方法!!

 

意外とこれが正解であることも多いので

ちょっと複雑な問題でも貪欲法から考えていくと

正解にたどり着けたりするとかしないとか。

それではコンテストです!

deck

By Seiji M

deck

  • 350