-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbasics.tex
5367 lines (4492 loc) · 230 KB
/
basics.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{入出力と配列・シミュレーション}\label{chapter:io}
\begin{itembox}[l]{概要}
様々な解法につながる基本として,入力データを逐一調べる問題から始めて,配列と周辺の基本操作をとりあげる.
また,プログラムを一度に作成するのではなく,部品ごとに作ってテストするスタイルを身につける.
さらに,データの量に応じて,適切なアルゴリズムが必要になることを経験する.
\end{itembox}
\section{最大値,最小値}
\begin{psbox}{ICPC Score Totalizer Software}{国内予選2007}
入力として与えられる数値列から最大値と最小値を除いた平均値を求めよ.
入力はそれぞれが競技者の演技ひとつに対応するいくつかのデータセットからなる. 入力のデータセット数は20以下である.
データセットの最初の行はある演技の採点に当たった\uline{審判の数 n} ($3 \le n \le 100$) である.引き続く n 行には各審判のつけた \uline{点数 s} ($0 \le s \le 1000$) がひとつずつ入っている. n も各 sも整数である.入力中にはこれらの数を表すための数字以外の文字はない.\sout{審判名は秘匿されている.}
入力の終わりはゼロひとつの行で示される.
\aojid{1147}
\end{psbox}
``Sample Input''を順に解釈すると,以下のように4つのデータセットからなることが分かる.
\begin{itemize}
\item (N=) 3 (S=) 1000 342 0
\item (N=) 5 (S=) 2 2 9 11 932
\item (N=) 5 (S=) 300 1000 0 200 400
\item (N=) 8 (S=) 353 242 402 274 283 132 402 523
\item 0 (入力の終わり)
\end{itemize}
\begin{alltt}
#--------
3 # データは3つ, N=3
1000 # 最大値
342
0 # 最小値
#--------
5 # データは5つ, N=5
2 # 最小値
2
9
11
932 # 最大値
#--------
5 # データは5つ
300
1000 # 最大値
0 # 最小値
200
400
#--------
8 # データは8つ
353
242
402
274
283
132
402
523
#--------
0 # これでおしまい
\end{alltt}
同様に``Sample Ouput''を解釈すると,上記の各データセットに対して一つ数
値が出力されていることが分かる.
\begin{itemize}
\item 342 (そのまま)
\item 7 (2, 9, 11の平均)
\item 300 (300, 200, 400の平均)
\item 326 (...の平均)
\end{itemize}
\subsection{プログラム作成}
以下に,審判の得点の合計を計算するプログラムと最大得点を計算するプログラムを示す.(問題が求めるもの
は合計や最大値ではないので,これらは問題への直接の回答ではない).
必要ならばこれらのプログラムを参考に,問題への回答を作成せよ.
\begin{cbox}[emphstyle={[2]\graytext},emph={[2]iostream,using,namespace,std,cin,cout,endl}]
#include <iostream>
using namespace std;
int N, S;
int main() {
while (cin >> N && N>0) {
int sum = 0;
for (int i=0; i<N; ++i) {
cin >> S;
sum += S; // std::accumulateを使っても良い
}
cout << sum << endl;
}
}
\end{cbox}
\begin{cbox}[emphstyle={[2]\graytext},emph={[2]iostream,using,namespace,std,cin,cout,endl}]
// (一部略)
while (cin >> N && N>0) {
int largest = 0;
for (int i=0; i<N; ++i) { // std::max\_elementを使っても良い
cin >> S;
if (largest < S) largest = S;
}
cout << largest << endl;
}
\end{cbox}
C言語からC++に移行する場合は,(基本は共通なので)さしあたり\cplusplus{網掛け}部分を覚えて使えるようになると良い.
コンパイルの仕方などは\ref{section:commands}節を参照.
\begin{warningbox}{C言語禁止}
本資料を読み進める場合は,C言語では不十分で,C++の機能の一部を使う必要がある.必要な部分を少しづつ紹介するので,この時点で\texttt{iostream,cin,cout}などに慣れること.
\end{warningbox}
\begin{tipsbox}{関数を使おう}
\texttt{max,min,sum}などは関数を使っても良い.
\end{tipsbox}
\begin{pybox}
while True:
N = int(input())
if N == 0:
break
S = []
for i in range(N):
S.append(int(input()))
print(sum(S))
print(max(S))
\end{pybox}
なお,Python3で整数除算を行うには\texttt{/}に代えて\texttt{//}という演算子を用いる.
\subsection{動作テスト}
\subsubsection{サンプル入力を用いたテスト}
問題文中の``Sample Input''に対する出力が合うことを確認してみよう.
\subsubsection{審判データを用いたテスト}
この問題は審判が用いた秘密の入出力が公開されている.これを利用し
て, プログラムの誤りの有無をさらに手元で試験することができる.
(プログラムが``Sample Input'' には正しく振舞うが,他のデータには誤ることを発見できるかもしれない)
\begin{itemize}
\item 入力 \url{http://www.logos.ic.i.u-tokyo.ac.jp/icpc2007/jp/domestic/datasets/A/A1}
\item 出力 \url{http://www.logos.ic.i.u-tokyo.ac.jp/icpc2007/jp/domestic/datasets/A/A1.ans}
\end{itemize}
出力の一致を確認するには,\texttt{diff}コマンドを用いる.
実行方法などは\ref{section:commands}節を参照.
\begin{terminal}
$ python3 icpc.py < A1 > my-out.txt
$ diff -u my-out.txt A1.ans
\end{terminal}
\begin{terminal}
$ ./a.out < A1 > my-out.txt
$ diff -u my-out.txt A1.ans
\end{terminal}
\begin{warningbox}{後回し禁止}
この時点で(次の章に進む前に)リダイレクションと\texttt{diff}の操作を覚えること.
\end{warningbox}
\section{計算時間の見積と試行回数}
コンピュータが得意な解法は全部を力づくで試すことである.実際に多くの問題をそれで解くことが出来る.
コンピュータが得意な解法は全ての可能性を力づくで試すことである.実際に多くの問
題をそれで解くことが出来る.
全ての可能性を列挙するには,for文を用いたり,再帰を用いたりする.
\begin{pbox}{Tax Rate Changed}{国内予選2014}
2つの商品の消費税率変更前の税込合計価格を元に, 新消費税率での税込合計
価格が最大いくらになるかを計算するプログラムを作って欲しい. 1円未満切り捨て等の細かい条件は問題文を参照のこと.
制約(抜粋): $10 < s < 1000$, 商品の税抜価格は$1$円から$s-1$円のすべてを考慮に入れる
Time Limit : 8 sec, Memory Limit : 65536 KB
\aojid{1192}
\end{pbox}
(1) 2つの商品の価格の候補の組み合わせを全通り試す.(2) 候補が,変更前
の税率で合計価格になっているかを調べる.そうでなければ捨てる (3) 変更
後の税率での合計価格を計算する.最大値なら記憶する.
という方針で作ってみよう.
仮に税込みの価格を計算する\texttt{tax(rate, price\_without\_tax)}という関数があっ
たとすると,次のような構造になるだろう.問題文では小文字の$s$を使って
いるが,本資料のプログラムでは(i, j, kなどの変数と区別して問題文由来である目印に)大文字で表記する.
\begin{cbox}
int maximum = 0;
for (int i=1; i<S; ++i)
for (int j=i; j<S; ++j)
if (tax(X,i)+tax(X,j) == S) // (*)
maximum = max(maximum, tax(Y,i)+tax(Y,j));
\end{cbox}
なおこの例題はさらに,繰り返し回数を減らすための工夫を検討することもで
きるが,C++なら今回は必須ではない.
Pythonの場合は,多少の工夫が必要である.下記のコードでは,ある組$(a_0,b_0)$の税込価格が$S$を越えたら,$b>b_0$での組$(a_0,b)$の税込価格も$S$を必ず超えるので,検討不要であるという性質を利用した.
\begin{warningbox}{関数必須 (横着禁止)}
必ず関数 \texttt{tax}を作成すること.徐々に複雑なプログラムを扱う際に,関数の作成能力は重要となる.この時点で身につける.
\end{warningbox}
\begin{pybox}[emph={break}]
def tax(p,x):
return p*(100+x) // 100 # 整数除算
def solve(X,Y,S):
for a in range(1,S):
for b in range(1,S):
sum = tax(a,X)+tax(b,X)
if sum == S:
# 新税でのtax(a,Y)+tax(b,Y) について検討
if sum > S:
break # b増加ならばsum増加のため
return best
while True:
X,Y,S = map(int, input().strip().split(' '))
if X == 0:
break
print(solve(X,Y,S))
\end{pybox}
\begin{debugbox}{TLE (Time limit exceeded)}
Pythonの場合はC++よりも実行速度が遅くなるため,C++と同じ方針ではACを
得られない場合がある.例えば,上記のサンプルコードでは9,10行目に高速化のための工夫を施している.
\end{debugbox}
\begin{warningbox}{税込み v.s. 税抜き}
本資料では,税抜価格と税率から税込価格を求める関数を作成した.向きを反対にして,税込み価格から税抜価格を求めるアプローチはどうだろうか.実はそちら向きは罠が多数あるのでお勧めしない(興味がある場合はお勧めの方法でAC後に理由を考えてみよう).
\end{warningbox}
\paragraph{複数データセットの入力}
この問題ではデータセットが複数与えられうる.すなわち,データが与えられ
る間はそれを読み込んで解を出力し,データが終了したらプログラムを終了さ
せる必要がある.「入力の終わりは,空白で区切られた3つのゼロからなる行
によって示される. 」という条件と通常のデータセットでは\texttt{X}が正であることを活用し,以下の構造でプログラムを書くことを勧める.
\begin{cbox}
#include <iostream>
using namespace std;
int X, Y, S;
int solve() {
... // X,Y,Sについて,最大値を計算
}
int main() {
while (cin >> X >> Y >> S && X>0) {
cout << solve() << endl;
}
}
\end{cbox}
\texttt{while}文の条件の\texttt{cin >> X >> Y >> S}の部分は\text{cin}への参照を
返し,\texttt{bool}にキャストされた際に,\texttt{cin}が正常状態かどう
か,すなわち\texttt{X, Y, S}を正常に読み込めたかどうかを返す.入力ファイルが間違っていた(よくある場合は,タイプミスまたは違う問題の入力を与えた)場合はここが\texttt{false}になって終了する.読み込めた場合に,\texttt{X, Y, S}は処理すべきデータセットの\texttt{X, Y, S}を読んだ場合と,入力終了の目印の空白で区切られた3つのゼロを読んだ場合の両方がある.前者の場合は正の整数であるはずなので,0かどうかをテストして判別する.
\paragraph{AOJへの提出}
上記の方針でプログラムを作成し,AOJでacceptされることを確認せよ.
もしacceptが得られなかった場合は手元での検証が可能である.まず,入力
(\url{http://icpc.iisf.or.jp/past-icpc/domestic2014/qualify14_ans/A1})
と正答
(\url{http://icpc.iisf.or.jp/past-icpc/domestic2014/qualify14_ans/A1.ans})
をダウンロードしておく.
\begin{terminal}
$ ./a.out < A1 > my-output.txt
$ diff -u A1.ans my-output.txt
\end{terminal}
差があれば行数が表示される.
\paragraph{計算量の簡単な検討 (estimation of the number of trials)}
多くのコンテストの問題では実行時間に制限がついているので,実行
時間を見積もり間に合う解法を考案することが必要な場合もある.またコンテ
ストを離れて実用に用いるプログラムでも,何らかの実行速度に関する要請が
ある場合が多い.プログラムを書き終えてから速度に問題があることが分かる
と,書き直しが困難な場合もあるので,プログラムを書く*前*に何らかの見通
しを得ておくことが望ましい.
現実の計算機は複雑な装置であるから,正確な実行時間の予想は簡単ではない.よって単純なモデルに基づく目安を考える.
たとえば,プログラムを実行する過程で,加減乗除のような基本演算が何回行
われるかを数える.このモデルでは加算と除算では速度が異なりうるとか,同時に
二つの命令を実行される場合があるなどの,細かい点は無視している.目安で
あるので現実との対応関係は別に議論が必要だが,役に立つ場面も多い.
上記のプログラムで4行目(*)が最大何回実行されるか,考察しよう(厳密に求めなくて良い).
回数を($S$に依存するので),$f(S)$と表すとする.2行目の\texttt{for}ループ
が$S$回,3行目の\texttt{for}ループが最大$S$回繰り返すので,$f(S) \le
S^2 \le 1\,000^2=10^6$である.
\begin{table}[h]
\centering
\caption{\cemphp{C++}で,1秒間で実行可能な演算回数の目安 (\cite{book:pcc}を改変して転載)}
\label{table:empirical-estimation}
\begin{tabular}{rl}\hline
$1\,000\,000$ &余裕を持って可能\\
$10\,000\,000$ &たぶん可能\\
$100\,000\,000$&とてもシンプルな演算なら可能\\\hline
\end{tabular}
\end{table}
この回数($10^6$)を表\ref{table:empirical-estimation}に当てはめると,(tax関数が効率的に実装されていることを前提に)余裕を持っ
て,1秒間で実行可能と分かる.\footnote{注: この見積もりは1つのデータセットについてのものである.一方,問題全体では「入力は複数のデータセットからなる」ものを8秒間で回答する必要があるため,計算時間はデータセット数の上限を考慮して見積もる必要がある.データセットの数について記述がない場合は,厳密には問題の不備であるが,10個程度と考えて良い.}
表の数値は,実行環境に合わせて経験的に測定する必要がある.
目安として,最近の多くのコンピュータのCPUは1GHz ($=10^{9}$)程度で動作しているので,CPUが100サイクル以内に実行できる演算なら,1秒間に$10^6$回実行可能と期待できる.
本資料の範囲では,この表を信じて問題ない場合がほとんどであるが,
正確な予測のためには,キャリブレーションを行う.すなわち,作成した解法が入力に対応する数値$N$に関しておよそ何回の基本演算を行う
かを見積もったら,いくつかの$N$に対応する入力で実験し,実際の計算秒数
との対応を取る.たとえばメモリ参照やnew/deleteは,算術演算よりそれぞれ10倍,100倍以上に遅い場合がある.また,C, C++以外の言語では,実行により時間がかかることも多い.問題ごとの秒数制限と,オンラインジャッジのハードウェアを考慮して検討する.
\begin{tipsbox}{Pythonの場合はC++よりも実行速度が遅くなる}
問題によるが20-200倍余裕
をもって見積もると良い.オンラインジャッジで時間制限は
Pythonに適切とは限らないので,ジャッジデータをダウンロードして正答を確認できればそれでも良い.
\end{tipsbox}
\begin{pbox}{Space Coconut Grab}{模擬国内予選2007}
宇宙ヤシガニの出現場所を探す.エネルギーEが観測されたとして,
出現場所の候補は,$x+y^2+z^3=E$を満たす整数座標(x,y,z)で,さらにx+y+zの
値が最小の場所に限られるという.x+y+zの最小値を求めよ.
(x,y,zは非負の整数,Eは正の整数で1,000,000以下,宇宙ヤシガニは,宇宙最大とされる甲殻類であり,成長後の体長は 400 メートル以上,足を広げれば 1,000 メートル以上)
\aojid{2012}
\end{pbox}
まず,全部試して間に合うかを考える.
(間に合うならそれが一番実装が簡単なプログラムである)
変数x,y,zの動く範囲を考えると,それぞれx:[0,E], y:[0,$\sqrt{E}$],
z:[0,${}^3\sqrt{E}$]である.それら(x,y,z)の組み合わせは,Eの最大値は
1,000,000$=10^6$であるから,最大$10^6 \cdot 10^3 \cdot 10^2=10^{11}$で
ある.
制限時間が8秒であることを加味しても,この方針では間に合いそうにない.
減らす指針として,全ての(x,y,z)の組み合わせを考える必要はなく,
$x+y^2+z^3=E$を満たす範囲だけで良いことを用いる.たとえば,xとyを決める
と,Eからzは計算できる(zが整数とならない場合は無視して良い).この方針で
調べる種類は,(x,y)の組み合わせの種類である,$10^6 \cdot 10^3$となる.
この値はまだ大きいが,先ほどと比べると1/100に削減できている.上記と同様
に,xとzを決めてyを求める,z,yを決めてxを求めることもできる.それらの方
針の場合に,調べる組み合わせの種類を求めよ.一番少ないものが間に合う範
囲であることを確認し,実際に実装してAOJに提出して確かめる.
\begin{debugbox}{Python--TLE}
残念ながら上記の方針ではPythonでは時間制限を超過(TLE)する.対策として
は,2重ループを避けて\texttt{z}に関する1重ループのみにすれば良い.
単調性から\texttt{y}に関するループを省くことが出来る.
\begin{pybox}
import math
# ...(中略)...
y = int(math.floor(math.sqrt(E-z**3)+1e-7))
\end{pybox}
\end{debugbox}
\section{練習問題}
\begin{pbox}{Square Route}{模擬国内予選2007}
縦横1500本程度の道路がある街で,正方形の数を数えてほしい.
\aojid{2015}
\end{pbox}
注: 全ての4点を列挙して正方形になっているかどうかを試すと間に合わないので,工夫した方法が必要である.正方形を一つづつ数えると数が多すぎるので,一定の性質を持つ正方形をまとめて数えるような方法が必要となる.
ヒント: 後の章で扱う標準データ構造を活用する方法と,正方形の初等的な性質を利用する方法がある(\textcolor{white}{対角線を延長した直線を共通に持つ正方形}について考えてみよう).
\chapter{整列と貪欲法}\label{chapter:greedy}
\begin{itembox}[l]{概要}
ある基準に従ってデータを並べ替えることを整列(sort)という.
ほとんどの言語の標準ライブラリでは,整列の方法が提供されているので,ま
ずはそれの使い方を習得しよう.この資料では整列された結果
を得られれば良いという立場をとるが,整列の手法そのものに興味がある場合は,たとえ
ば~\pcaojbook[pp.~51--(3章)]を参照されたい.
データの整列は,問題解決の道具として活躍する場合もある.それらには,最適化問題や配置問題など,整列とは関係がない見た目の問題も含まれる.
\end{itembox}
\section{さまざまな整列}
\subsection{数値の整列}\label{section:sort}
C++とRubyでは,標準関数として\texttt{sort}や\texttt{sort!}が用意されている.
\begin{cbox}[emph={algorithm,sort}]
#include <algorithm>
#include <iostream>
using namespace std;
int A[5] = {3,5,1,2,4};
int main() {
sort(A,A+5); // 半開区間[l,r)で指定する.sort(\&A[0], \&A[5])と同じ意味
... // coutにAを出力してみよう
}
\end{cbox}
配列を整列させる範囲を指定するには,\texttt{\&A[0]}のように配列の要素を指すポインタを用いる.一次元配列の場合に単に\texttt{A}と書けば,先頭要素を指すポインタに自動的に変換される.
配列でなく\texttt{vector}や\texttt{array}の範囲を指定するには,
\texttt{A.begin(),A.end()}という表記を用いる.この\texttt{begin}や\texttt{end}はポインタを一般化したイテレータを返す関数で,名前の通りの場所を指し示す.他に\texttt{A.begin()+2,A.begin()+5}のように一部の区間を指定することもできる.さらにC++11以降では\texttt{begin(A)}とすることで配列もvectorも共通に扱うことができる.
\begin{pybox}[emph=sort]
a = [3,5,1,2,4]
a.sort()
a
# [1, 2, 3, 4, 5]
\end{pybox}
\begin{psbox}{Sort II}{AOJ}
与えられたn個の数字を昇順に並び替えて出力するプログラムを作成せよ.
\aojid{10029}
\end{psbox}
回答例:
\begin{cbox}
int N, A[1000000+10];
int main() {
cin >> N;
for (int i=0; i<N; ++i) cin >> A[i]; // Aの入力
... // Aをソートする
for (int i=0; i<N; ++i) cout << (i?" ":"") << A[i]; // Aの出力
cout << endl;
}
\end{cbox}
出力部分の\texttt{(i?" ":"")}は,要素の間にのみ空白文字を入れるための微調整(先頭\texttt{i==0}には空白を入れない).
\subsection{文字列の整列}
\begin{psbox}{Finding Minimum String}{AOJ}
N個の小文字のアルファベットのみからなる文字列の,辞書順で先頭の文字列
を求める.
問題文中に記述がないがNは1000を越えない.
\aojid{10021}
\end{psbox}
補足: 辞書式順序は,大文字小文字が混ざる場合はC++の\texttt{std::string}の比較演算子の比較順序とは異なる.(が,今回は小文字のみなのでその心配はない)
\begin{cbox}
#include <algorithm> // sortのため
#include <string> // string (文字列)のため
#include <iostream>
using namespace std;
string A[1000];
int N;
int main() {
cin >> N;
if (N > 1000) abort();
for (int i=0; i<N; ++i) cin >> A[i];
... // 整数の時と同様にAを整列(sort)する
cout << A[0] << endl;
}
\end{cbox}
\subsection{ペアと整列}\label{section:sort-pairs}
\subsubsection{ペア}
ペア(組み)の表現を導入する.$\langle$開始時刻,終了時刻$\rangle$や$\langle$身長,体重$\rangle$,$\langle$学籍番号,点数$\rangle$など,現実世界の情報にはペアとして表現することが自然なものも多い.
\begin{cbox}
#include <utility> // pairのため
#include <iostream>
using namespace std;
int main() {
pair<int,int> a(2,4); // 整数のペア
cout << a.first << ' ' << a.second << endl; // 2 4と表示
a.first = 3;
a.second = 5;
cout << a.first << ' ' << a.second << endl; // 3 5と表示
a = make_pair(10, -30);
cout << a.first << ' ' << a.second << endl; // 10 -30と表示
pair<double,char> b; // 小数と文字のペア
b.first = 0.5;
b.second = 'X';
cout << b.first << ' ' << b.second << endl; // 0.5 Xと表示
}
\end{cbox}
Pythonやruby の場合は列(配列やリスト)を手軽に使えるので,(取り立ててpairを区別せず)列を使う.
\subsubsection{ペアの配列と整列}
続いて,ペアの配列を扱う.たとえば,一人の身長と体重をペアで表現する時に,複数の人の身長と体重のデータはペアの配列と対応づけることができる.
前回,整数の配列を整列(\texttt{sort})したように,ペアの配列も整列することができる.
標準では,第一要素が異なれば第一要素で順序が決まり,第一要素が同じ時には第二要素が比較される.
\begin{cbox}
#include <utility> // pairのため
#include <algorithm> // sortのため
#include <iostream>
using namespace std;
int main() {
pair<int,int> a[3]; // 整数のペア
a[0] = make_pair(170,60);
a[1] = make_pair(180,90);
a[2] = make_pair(170,65);
for (int i=0; i<3; ++i) // a[0]からa[2]まで表示
cout << a[i].first << ' ' << a[i].second << endl;
// 170 60
// 180 90
// 170 65 と表示されるはず
sort(a, a+3); // a[0]からa[2]まで整列
for (int i=0; i<3; ++i) // a[0]からa[2]まで表示
cout << a[i].first << ' ' << a[i].second << endl;
// 170 60
// 170 65
// 180 90 と表示されるはず
}
\end{cbox}
\begin{pybox}
a = [[3,5],[2,9],[3,6]]
print(a) # [[3, 5], [2, 9], [3, 6]]
a.sort()
print(a) # [[2, 9], [3, 5], [3, 6]]
\end{pybox}
\subsubsection{3つ以上の組}
C++11では,3つ以上の組を表す\texttt{tuple}型が用意されている.\footnote{\url{http://en.cppreference.com/w/cpp/utility/tuple}}
それより前のC++では\texttt{pair<int,pair<int,int> >}などと\texttt{pair}を重ねて用いていたところが,簡潔に書けるようになった.
\begin{c11box}[emph={tuple}]
#include <tuple>
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
tuple<int,int,int> a = {3,1,4};
cout << get<0>(a) << "\n"; // 3
cout << get<1>(a) << "\n"; // 1
cout << get<2>(a) << "\n"; // 4
tuple<int,int,int> array[] = {{3,1,4}, {1,5,9}, {2,6,5}};
sort(array, array+3);
tuple<int,int,int> t = array[0];
cout << get<0>(t) << "\n"; // 1
cout << get<1>(t) << "\n"; // 5
cout << get<2>(t) << "\n"; // 9
}
\end{c11box}
\subsection{大きい順に整列}
標準の\texttt{sort}関数は小さい順に並べ替える.大きい順に並べ替えるに
はどのようにすれば良いだろうか?
\begin{enumerate}
\item 小さい順に並べ替えた後に,並び順を逆転させる (当面これで十分)
\begin{cbox}
int A[5] = {3,5,1,2,4};
int main() {
sort(A,A+5);
// C++11なら sort(begin(A),end(A));
reverse(A,A+5); // 与えられた範囲を逆順に並び替え
... // coutにAを出力してみよう
}
\end{cbox}
\begin{pybox}
a = [3,5,1,2,4]
a.sort()
a.reverse() # aを逆順に並び替え
\end{pybox}
\item \texttt{reverse\_iterator}を使う
\begin{c14box}[emph={rbegin,rend}]
sort(rbegin(A),rend(A));
\end{c14box}
\begin{pybox}
a.sort(reverse=True)
\end{pybox}
\item 比較関数を渡す (汎用的)
\begin{c11box}
int A[5] = {3,5,1,2,4};
int main() {
sort(begin(A),end(A),[](int p, int q){ return p > q; });
... // coutにA[i]を出力してみよう
}
\end{c11box}
文法の説明は省略するが\texttt{[](int p, int q)\{ return p > q; \}}の部分が2つの整数の並び順を判定する匿名関数である.
\begin{pybox}
a.sort(key=lambda e: -e)
\end{pybox}
\end{enumerate}
\section{貪欲法}
\subsection{良い順に使う}
\begin{pbox}{カントリーロード}{UTPC 2008}
カントリーロードと呼ばれるまっすぐな道に沿って, 家がまばら
に建っている. 指定された数までの発電機と電線を使って全ての家に給電したい.
家に電気が供給されるにはどれかの発電機に電線を介してつな
がっていなければならず, 電線には長さに比例するコストが発生する. できるだけ電線の長さの総計が短くなるような発電機 お
よび電線の配置を求める.
\aojid{2104}
\end{pbox}
ヒント: 発電機が一つなら,端から端まで全ての家を電線でつなぐ必要がある.
発電機が2つなら,端から端まで全ての家を電線でつないだ状態から,家と家
の間の一箇所に電線を引かずに節約することができる.つまり,一番長い一箇所を節約するのが得.
発電機が3つなら,端から端まで全ての家を電線でつないだ状態から,家と家
の間で一番長い部分と二番目に長い部分には電線を引かずに節約することができる....
\begin{tipsbox}{方針の確認}
プログラムを書く前にSample Inputを手で解いてみよう
\end{tipsbox}
回答例
\begin{cbox}
int T, N, K, X[100000+10], A[100000+10];
int main() {
cin >> T; // データセットの数を読み込む
for (int t=0; t<T; ++t) {
... // 家と発電機の数を読み込む
for (int i=0; i<N; ++i) cin >> X[i]; // 家の位置を入力
for (int i=0; i+1<N; ++i) A[i] = X[i+1]-X[i]; // 家と家の間
... // 配列Aを整列する
... // 配列Aの先頭max(0,N-1-(K-1))個の和を出力する
}
}
\end{cbox}
Python3の入力例: 与えられた1行を配列として読み込む場合は\texttt{list(...)}で変換する
\begin{pybox}
N,K = map(int,input().strip().split(' '))
X = list(map(int,input().strip().split(' ')) # リストへの変換を明示
\end{pybox}
\begin{debugbox}{よくある考え漏れ}
発電機\textcolor{white}{の個数が家の数を越えて増えても}節約できる区間は増えない (ヒントが白い文字で書かれているので,読みたくなったらコピーペーストなどで読むと良い).
\end{debugbox}
このような解法が貪欲法(greedy)と呼ばれる意味は,節約する区間の「組み合わせ」を
考えることなく,(長さ順に並べた)単独の区間を順に一つづつ見て閾値を越えるまで節約することを貪欲に決める点である.
\begin{pbox}{Stripies}{Northeastern Europe 2001}
質量が$m_1,m_2$である2体合体すると,$2\sqrt{m_1\cdot m_2}$となる種族がある.
初期状態を与えられるので,全てが合体した時の最小の質量を求めよ.
\url{http://poj.org/problem?id=1862}
\end{pbox}
小数点以下3桁を出力するには\texttt{printf("\%.3f\textbackslash{}n", ret);}を用いる.
\begin{pbox}{Princess's Marriage}{模擬国内予選2008}
護衛を上手に雇って,道中に襲われる人数の期待値を最小化する.
護衛は1単位距離あたり1の金額で,予算がある限り,自由に雇える.
入力は,区間の数N, 予算Mに続いて,距離と1単位距離あたりの襲撃回数の期
待値のペア$\langle$D,P$\rangle$がN個.
\aojid{2019}
\end{pbox}
考え方:
せっかく護衛を雇うなら,もっとも危険な区間を守ってもらうのが良い.
一番危険な道を選び,予算がある限りそこに護衛を雇う.予算の残額で道の区間全てをカバーできない場合は,安全になる区間と,危険なままの区間に道が分かれる.予算が残っている限り,2番目に危険な道,3番目に危険な道の順に同様に繰り返す.残った危険な区間について,期待値の和が答えとなる.
この計算課程では,$\langle$危険度,長さ$\rangle$のペアで道を表現し,危険な順に整列しておくと便利である. (ここで危険度は,距離1移動する間に襲われる回数の期待値を表す)
回答例 (入力と整列):
\begin{cbox}
int N, M;
pair<int,int> PD[10010];
int main() {
while (cin >> N >> M && N) {
int d, p;
for (int i=0; i<N; ++i) {
cin >> d >> p;
PD[i] = make_pair(p, d);
// PD[i].first は道iの危険度
// PD[i].second は道iの長さ
}
... // PDを大きい順に整列しよう
// 整列がうまく行ったか,PDを表示してみよう
// うまく整列できたら,次は答えを計算しよう
}
}
\end{cbox}
回答例 (答えの計算):
\begin{cbox}
int S = 0;
for (int i=0; i<N; ++i)
S += 道[i]の危険度 * 道[i]の長さ;
// 予算0の時の答えが,現在のSの値
for (int i=0; i<N; ++i) {
if (M <= 0) break;
int guarded = Mと道[i]の長さの小さい方; // 雇う区間
S -= 道[i]の危険度 * guarded;
M -= guarded;
}
S が答え
\end{cbox}
\begin{itemize}
\item 予算が0の場合は,答えとして必要な ``刺客に襲われる回数の期待値''である $S$は,各道$i$について$S=\sum_i P_i\cdot{}D_i$となる.
\item 予算がMの場合,$S$から護衛を雇えた区間だけ期待値を減らす.例えば
道$j$の区間全部で護衛を雇うなら $P_j\cdot{}D_j$だけ$S$を減らすことができる.もし残り予算$m$が道の長さ$D_j$より小さく道の一部だけしか護衛を雇えないなら$S$から減らせるのは$P_j\cdot{}m$だけである.
\end{itemize}
\subsection{区間スケジューリング}
「アルゴリズムデザイン」\cite{book:algorithmdesign}の4.1章(pp.~104--108)を参照.
\paragraph{問題概要}
一つの共有資源(体育館,駐車場など)と多数の利用申請(開始時刻と終了時刻
のペア)があったとする.使用時間が衝突しないように,利用申請の部分集合
を選ぶ.最大いくつの申請を採用できるか?
\centerline{\begin{tikzpicture}
\draw (3,3.5) -- node[above]{$$} (5,3.5) node[right]{$0$};
\draw (1,3) -- node[above]{$$} (3.5,3) node[right]{$1$};
\draw (6,2.5) -- node[above]{$$} (7,2.5) node[right]{$2$};
\draw (2,2) -- node[above]{$$} (4.5,2) node[right]{$3$};
\draw (1.5,1.5) -- node[above]{$$} (2.5,1.5) node[right]{$4$};
\draw (4,1) -- node[above]{$$} (7.5,1) node[right]{$5$};
\draw[color=gray,dotted,->] (0,0.5) -- (8,0.5) node[right] {time};
\end{tikzpicture}}
単純化: 体育館や時刻などの具体を削ぎ落とすと,線分の集合から一部を選択する問題になる.
\paragraph{直感と考察}
\begin{itemize}
\item 実は左から採用すれば良いのでは? \dingright{} そうでもない(反例有り)
\item 短い線分は他と重なりにくい.短い線分から採用すれば良いのでは? \dingright{} 必ずしもそうでもない(反例有り)
\end{itemize}
\paragraph{正しいアルゴリズム}
\begin{enumerate}
\item \cemph{右端が一番左にある線分}(i.e., 早く終わる申請)を選び,重なる線分とともに取り除く
\item 未選択の線分がまだあれば,step.1に戻り手順を繰り返す
\end{enumerate}
\paragraph{区間を扱う練習問題}
以下の問題は,区間スケジューリング問題とは異なるが,区間を扱う問題である.
\begin{pbox}{Cleaning Shifts}{USACO 2004 December Silver}
連続するT日の掃除当番を決めたい.各「牛」は何日目から何日目まで(境界を含む)区間働けることがわかっている.当番が不在の日の内容に牛を配置するとき,最低何頭必要か答えよ.不可能な場合は-1を出力せよ.
\url{http://poj.org/problem?id=2376}
\end{pbox}
ヒント: 当番に穴を空けない中で,もっとも遅くまで担当できる牛を採用してゆく.
\begin{tikzpicture}
\draw (1,2.5) -- (8,2.5);
\draw[color=ired] (1.5,2) -- (10,2) node[right] {best};
\draw (2,1.5) -- (5,1.5);
\draw (2.5,1) -- (7,1);
\draw[color=icyan] (3.2,0) -- (3.2,3) node[right] {$t$};
\draw[color=gray,->] (0,0.5) -- (14,0.5) node[right] {day};
\end{tikzpicture}
回答例 (入出力)
\begin{cbox}
int /*牛の総数*/N, /*掃除日数*/T;
pair<int,int> C[25010]; // C[i].firstが担当開始日,C[i].secondが担当終了日
int solve() {
// N,T,Cを元に回答を計算する
}
int main() {
scanf("
for (int i=0; i<N; ++i)
scanf("
printf("
}
\end{cbox}
回答例 (計算)
\begin{cbox}
int solve() {
sort(C, C+N); // 掃除開始日の早い順に整列
int /*牛番号*/i = 0, /*次の担当の掃除開始日*/t = 1, /*採用数*/c = 0;
while (i<N && t<=T) {
int best = 0; // 次の雇う予定の牛の担当終了日
while (i<N && 牛iの担当開始がtより遅くならない間) {
... // 牛iの担当終了がbestより遅ければbestを更新
++i;
}
if (担当可能な牛が一頭も居なければ(best<t)) return -1;
t = best+1;
++c;
}
return /*Tまで掃除終了しているか?*/ ? c : -1;
}
\end{cbox}
戦略: $t-1$日目まで掃除が終わっていたとする.$t$日目を含む区間に掃除をできる牛がいなければ,解はない(-1を出力).もし複数の牛がt日目を担当可能であれば,なるべく長く担当してもらえる牛を雇うのが良い(*).その牛が$s$日まで担当したとすると,$t=s+1$として,全体の区間が終わるまで,牛を雇い続ける.
他の牛を雇っても最適解になる可能性はあるが,(*)の戦略をとった場合と比べて,雇う牛の数を減らすことはできないことが証明できる.
\begin{pbox}{Radar Installation}{Beijing 2002}
全ての島が見えるようにレーダーを配置する
注意: y=0の島が存在する模様
\url{http://acm.pku.edu.cn/JudgeOnline/problem?id=1328}
\end{pbox}
ヒント:
\begin{itemize}
\setlength{\itemsep}{0pt}
\item 島が観測できるレーダーの区間を列挙したとする.まだレーダにカバーされていない島を一番沢山カバーできる場所にレーダを配置し,それを繰り返すという戦略を考える.この戦略が最適な配置より多くのレーダを必要とするような島の並びの例を示せ
\item 左から順にレーダを配置するとする.(まだレーダにカバーされていないなかで)見えはじめるのが最も左にあ
る島を選び,その島が見える区間の左端にレーダを配置するとする.この戦略が最適な配置より多くのレーダを必要とするような島の並びの例を示せ
\item 左から順にレーダを配置するとする.(まだレーダにカバーされていないなかで)見えはじめるのが最も左にあ
る島を選び,その島が見える区間の右端にレーダを配置するとする.この戦
略が最適な配置より多くのレーダを必要とするような島の並びの例を示せ
\item 左から順にレーダを配置する正しい戦略Aを示し,他のどのような配置も戦略Aによる配置よりレーダ数が小さくならないことを証明せよ
\end{itemize}
\begin{debugbox}{よくあるバグ}
遠すぎる島など,例外を考慮する必要がある.複数のテストケースを扱うので,遠すぎる島があっても入力は全ての島を読み込むこと(さもないと次のテストケースで先頭がずれる).
\end{debugbox}
\subsection{様々な問題}
\begin{pbox}{Make Purse Light}{模擬国内予選 2005}
財布の中身を軽くする
\aojid{2007}
\end{pbox}
\begin{pbox}{Fox and Card Game}{Codeforces 388 C}
先手は山の一番上からカードをとり,後手は一番下から取る時,それぞれが最善を尽くした時の得点を求める.
\url{http://codeforces.com/problemset/problem/388/C}
\end{pbox}
\begin{pbox}{Shopping}{アジア大会2014}
直線上に配置された複数の店を制約を満たしながら買い物をして左から右に抜ける時の,必要な移動の最小を求める.
\aojid{1347}
\end{pbox}
\begin{pbox}{Dinner$\star$}{夏合宿2014}
食堂の食事と自炊を組み合わせてN日間の幸福度を最大化する.
\aojid{2642}
\end{pbox}
\begin{pbox}{Ploughing$\star\star$}{13th Polish Olympiad in Informatics}
畑を,縦(幅1列)か横(1行)にスパっと切りとることを繰り返して,区分けする.
どの範囲も数の合計がK以下になるようにする.条件を満たす最小の分割数を求めよ.
\url{https://szkopul.edu.pl/problemset/problem/6YiP6JA5U15hY94pLwuHoYPg/site/}
\end{pbox}
\chapter{動的計画法 (1)}\label{chapter:dp}
\begin{tabular}{@{}cc@{}}
\begin{minipage}{.6\linewidth}
\begin{itembox}[l]{概要}
全ての可能性を調べ尽くすことが難しいような問題も,小さな問題を予め解いて解を表
に覚えておくなどの整理を適切に行うことで簡単に解けるようになる場合もある.
大小の問題の関係を表す式を立てて,それをプログラムにしてみよう.
動的計画法は,問題が持つ部分構造最適性を利用して効率の良い計算を実現する方法である.
\end{itembox}
\end{minipage}
&
\begin{minipage}{.35\linewidth}
\includegraphics[width=.45\linewidth]{DSC_0537s.JPG}
\includegraphics[width=.45\linewidth]{DSC_0541s.JPG}\\
\scriptsize フィボナッチ聖人の彫像\hfill(Camposanto, Pisa)
\end{minipage}
\end{tabular}
\section{数を数える}
\begin{psbox}{Fibonacci Number}{AOJ}
N番目のFibonacci数を求めよ。
\url{http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_10_A}
\end{psbox}
簡単のため$i$番目のFibonacci数を$F_i$と表記すると,以下のように定義される($F_0=0$と定義することも多いが本質ではないので問題文に合わせる):
\begin{equation}
F_i = \left\{
\begin{array}{ll}
1 & i=0\\
1 & i=1\\
F_{i-1}+F_{i-2} & \mbox{(otherwise)}
\end{array}\right.\label{eq:fib-recurrence}
\end{equation}
上記の定義をそのまま再帰で定義すると以下のようになるだろう:
\begin{pybox}
def fib(n):
print("fib",n) # 関数呼び出しの際に引数を表示 (動作確認後に消す)
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-2)+fib(n-1)
\end{pybox}
しかし,この方法は計算の重複が多く,効率的でない.nの増加に伴い急速に遅くなる.
一例としてpythonの\tindex{timeit}モジュールを使った実行時間測定例を示す.
\begin{pybox}[emph=timeit]
import timeit
print(timeit.timeit("fib(10)", globals=globals(), number=10))
print(timeit.timeit("fib(20)", globals=globals(), number=10))
print(timeit.timeit("fib(30)", globals=globals(), number=10))
\end{pybox}
\begin{terminal}
$ python3 fib.py
0.0001415439764969051
0.01775405099033378
2.1516372300102375
\end{terminal}
今回主として扱う,より素直な方法は,小さいフィボナッチ数から順に計算す
ることである.
式(\ref{eq:fib-recurrence})から,$F_i$を求めるには,$F_0$から$F_{i-1}$までの値のみが必要であり,それらがあれば加算1回で計算できるという関係がわかる.そこで$F_0$から順に計算すれば,各$F_i$は加算1回で求められる.
\begin{cbox}
int F[100]; // 必要なだけ
int main() {
F[0] = 1;
F[1] = 1;
for (int i=2; i<45; ++i) { // 必要なだけ
F[i] = F[i-2]+F[i-1];
}
}
\end{cbox}
この章で扱う問題は,求めたい問題の答え(e.g., $F_{100}$)が\jindex{部分問題}{ぶぶんもんだい}の答え($F_{n}, \; n<100$)から効率的に計算可能なものを取り扱う.
今回の場合は,回答にあたって直接必要なものは$N$番目のデータだけだが,一見回り道のようでも$N$番目*まで*のデータを全て計算しておくアプローチが有効である.
全体として必要な基本演算の回数(以下,単に計算量と表記する)は$O(N)$である.なお,メモ化や繰り返し自乗法による$O(\log N)$の計算方法については\footnote{簡単のために,この資料では整数演算のコストを定数と扱っている.しかし,多倍長整数を使う場合は,大きな数の演算は遅くなることに注意.},\ref{chapter:rsquares}章を参照されたい.
この節の目標は,部分問題の解の関係を表す,式(\ref{eq:fib-recurrence})のような漸化式から,解を求めるプログラムを書けるようになることである.
\begin{psbox}{Kannondou}{PC甲子園2007}
階段を1足で1,2,3段上がることができる人が,$n$ ($<30$)段登るときの登り方の数を,
適当に整形して出力する.
\aojid{0168}
\end{psbox}
各段への登り方の数を配列で表現する.
漸化式: 部分問題として,下から$i$段目に到達する登り方を$A_i$通りとする.登る前は
$A_0=1$, 最終的に知りたいのは$A_n$である.\\
\begin{equation}
A_i = \left\{
\begin{array}{ll}
1 & i=0\\
A_{i-1} & i=1\\
A_{i-1}+A_{i-2} & i=2\\
A_{i-1}+A_{i-2}+A_{i-3} & \mbox{(otherwise)}
\end{array}\right.
\end{equation}
フィボナッチ数の計算と同様に,$N$段に対する答えを求める計算量は$O(N)$
となる.
回答例 (数の計算)
\begin{cbox}
#include <algorithm>
#include <iostream>
using namespace std;
int A[128], N;
int main() {
A[0] = 1;
for (int i=1; i<=32; ++i) {
A[i] = A[i-1];
if (...) A[i] += A[i-2];
if (...) A[i] += A[i-3];
}
// A[.]を適当に出力してみる
}
\end{cbox}
\begin{pybox}
A = [0 for _ in range(32)]
A[0] = 1
for i in range(1,32):
A[i] = A[i-1]