計算量について
アルゴリズム
ある処理を行うプログラムを作るとき、どのように処理を行うのかという計算手順のことをアルゴリズムと呼びます。
- 例: 1 から n までの和を求めるプログラム
- 1 から n までの数を順番に足していく : n-1 回の計算を行う
- 等差数列の公式を用いて を計算する : 3 回の計算を行う
このように、同じ処理を行うプログラムでも、複数のアルゴリズムを考えることができます。コンピュータも 1 回の計算には時間がかかるので、処理を早く終わらせるためには、より効率の良いアルゴリズムを考える必要があります。アルゴリズムの大まかな性能を表す指標として、計算量というものがあります。
計算量
プログラムは入力に対して必要な計算を行い結果を出力する。この時の計算時間や記憶領域の量がどのくらい変化するのかが計算量です。計算量には、時間計算量と空間計算量があります。
- 時間計算量: 計算の回数
- 空間計算量: 必要な記憶領域の量
たんに計算量という時は、時間計算量をさすことが多いです。
先ほどの 1 から n までの和を求めるプログラムの例で考えてみよう。
#include <bits/stdc++.h>
using namespace std;
int main(){
int N;
cin>>N;
int sum=0;
for(int i=1;i<=N;i++){
sum+=i;
}
cout<<sum<<endl;
}
このプログラムでは、for 文を n 回繰り返しているので、計算回数はおおよそ n 回になります。このとき時間計算量はオーダー記法を用いて と表します。このように、計算量を表すときには、計算回数ではなく、計算回数のオーダーを表します。
オーダー記法
厳密に時間計算量、空間計算量を求めることは大変です。そこでざっくりと計算量を表すために、オーダー記法 を用います。
- 例: という式があったとき、 と表す。
- 定数倍は無視する
- N が大きくなった時、最も影響が大きい項のみを考える
の時、 , であり、 は の量を無視できるほど大きいので と表します。 を無限大に近づけた時の発散スピードを考えるとイメージしやすいかもしれません。
この図を見ると計算量の大まかなイメージがつかめると思います。
例題
以下の式のオーダー記法を答えよ。
答え
コードを見て、計算量を考えよう
#include <bits/stdc++.h>
using namespace std;
int main(){
int N;
cin>>N;
int sum=0;
for(int i=1;i<=N;i++){
sum+=i;
}
cout<<sum<<endl;
}
答え
さっき出てきたコードを同じです。これは for 文で処理を 回繰り返しているので、 であらわされます。
#include <bits/stdc++.h>
using namespace std;
int main(){
int N;
cin>>N;
int sum=0;
for(int i=1;i<=N;i++){
for(int j=1;j<=N;j++){
sum+=i*j;
}
}
cout<<sum<<endl;
}
答え
このコードは for 文が 2 重になっているので、 であらわされます。
#include <bits/stdc++.h>
using namespace std;
int main(){
int N;
cin>>N;
int count=0;
while(N>0){
N/=2;
count++;
}
cout<<count<<endl;
}
答え
このコードは を で割り続けているので、 を割る回数を とすると で 表せます。この式の両辺を をとると、 となります。 よって、 であらわされます。 計算量の は底はなんでもいいです。底の変換公式より、 なので、定数とみなせます。 今回は、 になります。
#include <bits/stdc++.h>
using namespace std;
int main(){
int N;
cin>>N;
int count=0;
vector<int> a(N);
rep(i,N){
cin>>a[i];
}
do{
count++;
}while(next_permutation(a.begin(),a.end()));
cout<<count<<endl; //N!
}
答え
このコードは、 個の数列の順列を全て列挙しています。順列の数は なので、 であらわされます。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a.at(i);
sort(a.begin(), a.end());
for (int i = 0; i < n; i++) cout << a.at(i) << " \n"[i == n - 1];
return 0;
}
sort の計算量はいくらでしょう。下記のレファレンスを読んでみると... https://cpprefjp.github.io/reference/algorithm/sort.html
答え
と記載があります。二つの for 文は なので、全体の計算量は になります。
#include <bits/stdc++.h>
using namespace std;
int f(int x) {
int sum = 0;
for (int i = 0; i < x; i++) sum += i;
return sum;
}
int main() {
int n, m;
cin >> n >> m;
int sum = 0;
for (int i = 0; i < n; i++) sum += f(i);
cout << sum << endl;
return 0;
}
答え
このコードは、 を 回呼び出しています。 の計算量は なので、 であらわされます。
競プロでの計算量
「実行時間制限」、「メモリ制限」があるので計算量をよく考えます。 基本的には、1 秒あたり ~ あたりまでは許されます。
- : まあ行ける
- : 単純な処理、加算、減算くらいならいける
- : まあムリ
目安
制約 | 間に合うオーダー | 使うアルゴリズム例 |
---|---|---|
順列全探索 | ||
bit 全探索 | ||
ワーシャルフロイド | ||
二重ループ | ||
ソート | ||
線形探索 | ||
素数判定 | ||
それ以上 | 二分探索 | |
定数時間 |
練習問題
解説
- f0
int f0(int N) {
return 1;
}
これは です。 に関係なく、常に 1 回の計算で終わります。
- f1
int f1(int N, int M) {
int s = 0;
for (int i = 0; i < N; i++) {
s++;
}
for (int i = 0; i < M; i++) {
s++;
}
return s;
}
これは です。 と の値によって計算回数が変わります。
- f2
int f2(int N) {
int s = 0;
for (int i = 0; i < N; i++) {
int t = N;
int cnt = 0;
while (t > 0) {
cnt++;
t /= 2;
}
s += cnt;
}
return s;
}
これは です。 を 回繰り返しているので、 になります。
- f3
int f3(int N) {
int s = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
s++;
}
}
return s;
}
これは です。 に関係なく、9 回の計算で終わります。
- f4
int f4(int N) {
int s = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
s += i + j;
}
}
return s;
}
これは です。 を 2 重に繰り返しているので、 になります。
- f5
int f5(int N, int M) {
int s = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
s += i + j;
}
}
return s;
}
これは です。 と を 2 重に繰り返しているので、 になります。
bit 演算
bit
コンピュータは、0 と 1 の 2 進数で情報を表します。0 か 1 の 2 通りで表せる情報の最小単位をbit
と呼びます。bit は、binary digit
の略です。
bit 演算子
bit 演算で主に使うものは以下の 6 つ
- AND:
&
- OR:
|
- XOR:
^
- NOT:
~
- 左シフト:
<<
- 右シフト:
>>
bool の AND&&
,OR||
との区別を理解しておこう。
特に、bit ごとの演算は「bitwise 〇〇」という言い方をする。
AND
2 つの bit がともに 1 のときのみ 1 を返す演算子です。
x | y | x & y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
- & &
- & &
OR
2 つの bit のどちらかが 1 のとき 1 を返す演算子です。
x | y | x | y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
- | |
- | |
XOR
2 つの bit が異なるとき 1 を返す演算子です。
x | y | x ^ y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
- ^ ^
- ^ ^
NOT
bit を反転させる演算子です。
x | ~x |
---|---|
0 | 1 |
1 | 0 |
- ~ = ~ =
左シフト
bit を左にシフトさせる演算子です。右側に 0 が追加されます。
x<<n
と書いたとき、 に を掛けることと同じです。
- << = << = =
右シフト
bit を右にシフトさせる演算子です。左側に 0 が追加されます。
x>>n
と書いたとき、 で割ることと同じです。ただし、小数点以下は切り捨てになります。
- >> = >> = =
複合代入演算子
演算 | 演算子 |
---|---|
AND | &= |
OR | |= |
XOR | ^= |
左シフト | <<= |
右シフト | >>= |
NOT の複合代入演算子はありません。
bit 演算の注意
符号あり整数型の場合(int,long long)、最上位 bit は符号を表すので、想定外の処理が起こることがあります。 bit 演算を行うときは、気を付けて演算をするか、符号なし整数型(unsigned int,unsigned long long)を使うか後で説明する bitset を使いましょう。
bit 全探索
AtCoder でよく出ます。bit 全探索とは、bit を使って全探索を行う方法です。bit 全探索は、 通りの組み合わせを全て試すことができます。シフト演算と AND 演算が理解できていれば分かります。
例題
の中から自由に数字を選び、選び出した数字の和でいくつの数字を作ることができるか。一つも選ばない場合は 0 になる。
考え方
3 つのこの数字を選ぶ、選ばないの 2 パターンを bit で表す。 から まで(今回は まで)調べる。
数字 | 4 | 2 | 1 | 作られる数字 |
---|---|---|---|---|
x | x | x | 0 | |
x | x | o | 1 | |
x | o | x | 2 | |
x | o | o | 3 | |
o | x | x | 4 | |
o | x | o | 5 | |
o | o | x | 6 | |
o | o | o | 7 |
コード
#include <bits/stdc++.h>
using namespace std;
int main(){
vector<int> a={1,2,4};
int n=3;
for(int bit=0;bit<(1<<n);bit++){ // 1<<nは2^n
int sum=0;
for(int i=0;i<n;i++){
if(bit&(1<<i)){ // bitのi番目のbitが立っているかどうか
sum+=a[i];
}
}
cout<<sum<<endl;
}
}
練習問題
ヒント
- 問題の言っていることは、選び出した集合の中に から までの数字が全て含まれていますかということ。
- 集合は set で扱います。
解答例
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n, m;
cin >> n >> m;
vector<set<int>> a(m);
for (int i = 0; i < m; i++)
{
int size = 0;
cin >> size;
for (int j = 0; j < size; j++)
{
int x;
cin >> x;
a[i].insert(x);
}
}
int ans = 0;
for (int bit = 0; bit < (1 << m); bit++) // 0 ~ 2^m - 1くりかえす
{
set<int> s;
for (int i = 0; i < m; i++)
{
if (bit & (1 << i)) // bitのi番目のフラグが立っているかどうか
{
for (auto x : a[i])
{
s.insert(x);
}
}
}
if ((int)s.size() == n) // setは重複を許さない集合なので,setのsizeが
// nであるとき1からnまでの全ての数字が含まれている
{
ans++;
}
}
cout << ans << endl;
}
bitset
- int 型は bit, long long 型は bit までしか扱えない。
- bitset は,任意の長さの bit 列を扱うことができる。
宣言
bitset<長さ> 変数名; // 長さは定数である必要がある
bitset<n> bs; // このような書き方はできません
bitset<4> bs; // 4bitのbit列
bitset<4> bs("1010"); // 4bitのbit列で初期化
代入
bs=5; // 5=101
bs=bitset<4>("1010");
bs=0b1010; // 0bで2進数を表すことができる
演算
bitset は bit 演算の演算子はすべて使える。
メソッド
メソッド | 説明 |
---|---|
.count() | 1 の数を数える |
.set() | 全ての bit を 1 にする |
.set(i,j) | i 番目の bit を j にする |
.reset() | 全ての bit を 0 にする |
.reset(i) | i 番目の bit を 0 にする |
.flip() | 全ての bit を反転する |
.flip(i) | i 番目の bit を反転する |
.to_string() | bitset を文字列に変換する |