AHC031 日記

注意

  • ほとんどコンテスト中に書いたもの(一部修正)なので、文体などが終わっています
  • いきなり関数名や変数名などが出てくるので、この日記を読むだけでは何を言っているかわからない場合があります
  • 寝たかどうかで日付を変えたのでX日目はX日目~X+1日目の午前くらいまで含まれる場合があります(生活習慣???)

1日目(3/22)

うお~~~
インタラクティブではなさそう
時系列系(?)か~
外周が使えるのがややこしそう
仕切りを実際に置くわけではなく長方形の端点を出力するのか
前作った仕切りがそのまま使えたら嬉しそう
仕切りの移動は長さに比例してコストがかかるけど区画不足は不足分の面積に比例して(*100もされる)コストがかかるのでうまく仕切りを移動させたほうがよさそう
とりあえず仕切りを移動させるコストを無視して日にちごとに区画不足にならないようにするとこれはほぼAHC001か
AtCoder Heuristic Contest 001 (AHC001) 初心者向け解説 - TERRYのブログ
を読みながら雑に遷移を書いた
う~ん 衝突部分で全体的にずらすみたいなのがうまくいっていなさそう
隙間を埋めると得点アップさせたい
実は左右を結ぶ線とそれを結ぶ縦線だけでどうにかなるとか
意外とどうにかなった(びっくり)
|    |
|----|
|    |
|    |
|----|
|    |
|    |
みたいな感じで横線を最初に決め打てばいい感じになる?(スコアを真面目に計算したくない(カス))
というか初日にmax(Ai,j)をiごとにとっていい感じに配置すればタダで済むじゃん
スコア1が達成可能っぽいseedは3000ケースのうち23ケースあった(seed40←嘘すぎ seed32だが???これを書いたのは誰ですか??? とか) 割とある
スコア計算は長方形の四辺を見ればよいのでO(W2+DNW)くらいでできるのか
使ったマスをxorすれば1を数えればよくなるのでbitsetでできそう
横線だけを重ねるの、かなり厳しそう
もしかして分割統治でうまくいく?
縦と横でDPする謎案が生えた 面白そうなので実装
DPしても多少収まらないseedがある でも制約では収まるとは書いてないから仕方ないのか?
上から貪欲にやってるのは改善の余地がありそう
とりあえずDP+山登りを提出したら28位(絶対50489614点、相対9,321,825,027点) 絶対スコアがみるみる下がって楽しい
でも2~4時間くらいでもっと上位の人がいるので絶対もっといい方法があると思う
雑にmaxを取って動かさないようにしたら18/50ケースで最小値になっていてそんな…という感じに
やっぱり動かさないことが大事っぽい
う~ん 1行で最適な動かさない方法は区間DPでできる…? 無理かも

2日目(3/23)

横線が動くの、かなりコストがかかる感じがする
区間のmaxを取って固定する方針だと、N個の面積の値はD2通りあって区間DP的なことをしようとするとO(D4)かかる
いや区間のマージの中心は固定だからD3でできるか?どちらにせよO(D2W2N)のDPは厳しそう
最終的に、基本長方形は動かさずにそのまま使って困ったら長方形を増やしたり減らしたりして対応させたい

3日目(3/24)

昨日なんもせずに寝ちゃった(?) 縦線を最初に固定したら横線を移動する回数が少なくなる? 縦線の挿入/削除/ずらしと長方形の左上の移動で焼ける? 詰め込みはAHC025の貪欲(大きい方からやる)が使えそう
なんか今回総体制という感じで面白いな
分割、あんまり山登れん
std::set::merge、要素を移動するのか~~~い 初めて使う関数はよく確かめよう!(一敗)
うお~どうにかbitsetで実装して提出したら22位(絶対24012026点、相対13,450,273,779点)
そういうことなのか~~~ やっぱり動かさないって大事すぎる
昨日までに書いたコード、13/100しかmaxじゃないらしいです そんな…
貪欲をDPにしたけどあんまり変わらなかった(多少上がった) 多分横を合わせるのは難しいということだと思う
次は幅をいじりたい
すみません、ビンパッキングの貪欲が間違っていました(え?)
とりあえずパッキング問題 | opt100に書いてあったBFDにした まあまあよさそう(最初の貪欲は何だったんだ…?→これNFDかも)
悪化した そんな…
このままビンパッキングを頑張ってもよいがそこまで大事ではなさそうなので保留
なんか思ったより時間余るな(解ができないやつもたまにあるが) う~~~ん

5日目(3/26)

4日目、消滅…(?)
どうにか前作った長方形をできる限り壊したくない
縦の破壊とかも考えたけどその縦線から出る横線が壊れるから微妙か
スコアが「線が重なれば重なるほど良い」から焼きなましで横線の位置をずらすのはスコアがすぐ変わって微妙?
横線の近さをスコアにすればよい?
でも近さ1と近さ0では雲泥の差だからなあ~
あと単純に遷移がわからん a:bにしている横線をランダムに持ってきてa:bで挿入する?
作った盤面とAを最大マッチング(問題の面積コストを最小化)しないといけない
多分B - 展覧会 (Exhibition)の価値が0、大きさが負?のバージョンなので
JOI 本選 2019 B - 展覧会 (AOJ 0659, 難易度 7) - けんちょんの競プロ精進記録を信じて昇順ソートして近いほうから貪欲でいいはず…
雑山登りを出したらまあまあ上がった 40位(絶対23346119点、相対10,199,430,378点)
自分が昔書いた謎DPの中身を思い出せなくなっていた 危ない…(1日毎に二つに分けてDPして積んでいる)
前の日をコピーする遷移とかはどうだろうか
前後をコピー、かなりいいかもしれん(まだわからないが、seed0で1001点を出していて素晴らしい)
いやこれやっぱり形の維持が重要だわ とりあえず高速化します
あと幅いじるのも後で入れてみよう
実行時間10倍で焼きなましするとseed1で100000点くらいまで行くらしい ホント???
さすがに嘘でした スコア計算がバグってた…
というか急にスコアが大きく変わるのであんまり焼きなましに向いてないなこれ スコアをいじるか?
結構頑張って高速化したけどまだ山登れるやつもあるらしい そんな(多少嬉しくはある)
!!! 提出したら21位(絶対21773976点、相対15,699,031,060点)!!! うおおおおおおおおお
謎DPから横線の山登りに繋げられるなら繋げてみたが微妙 時間管理がうまくいってない?
乱数をいじったりしたけどやっぱり普通にスコア計算が遅そう
+-1させる遷移から前後の日の横線に合わせる遷移に変えたら16位(絶対21403191点、相対17,552,957,603点) よしよしよしよしよし
震えが止まらん やべ~~~~~ でもあと5日あるんだよな…
う~むこの方針だと頑張っても20G~25G止まりでは…?
あ~なんか真面目に集合を持っておけばどうにかなるかも 線の移動はその線の階層だけ見ればいいし

6日目(3/27)

定数倍があるにしてももう少し回ってもよくない?
あとおさまりきらないやつも山登りさせてみたい
今一回の更新にO(NlogN)くらいかかってるけどseed1はN=36なのに3秒近く回しても8e5回くらいしか回ってないのはなんでだ~~~
ラムダ式のほうが関数より速そう
C++の関数オブジェクトが関数よりどれだけ速いか - でも今日はSRMあるからに関数オブジェクトのほうが速いって書いてあるけどこれは再帰だから?
なんか思ったよりD日全部で固定可能な長方形があるらしい へ~ 実装してみるか…
やりたいこと:

  • 無理やり全日で固定できるやつを固定する(実装が大変なことになりそうなのでWとNとAを書き換えてごまかす)→ダメだった そもそも入りきるかわからないし入りきったとしてもよくなるとは限らない(幅が大きくなってしまうので)
  • わざと小さい幅にして山登りに託す

7日目(3/28)

同じ幅の中で並び替えるという近傍を思いついたのでやってみる
かなり良かった 17位(絶対11692752点、相対19,398,314,154点)に浮上…
でもこれを頑張っても25Gくらいにしかならないのでは?という気持ちがある
メモ:最終提出ではassertは切る、rand_int()のsource_locationも切る
変えても絶対改善しない日は改善しないようにしたい
改善する確率が低そうな遷移をなくしたら相対スコア下がった 多分スコアが小さいところで負けてるんだと思う
改善しない日は避けるようにしたら多少上がった
vector<vector<int>>とvector<rectangle>の比較はvector<rectangle>の方が速いらしい へ~
適当に破壊して貪欲に埋めるみたいな遷移が必要かもしれん
なんかsortからinsertに遷移を直したのが消滅しとる まあいいか…
あ~ 本当は焼きなましをしたいが…
横線の移動、線が消える+線が増えるの二つの差ができていて微妙 本当は線を増やす→線を消すみたいなことをしたいがどうやってスコア計算すればいいのかわからん
風呂、考察がはかどる(風呂で考察したみたいなのを読んだ気がする)
まず縦を焼いてから横にいくといいかも
あとfake_score_diffはたぶん差分計算がもっとできる(変えた横線だけ見ればよい)

8日目(3/29)

cout << hoge << '\n';の後にassertを書いたのでcoutされないというバグをやった
ずっと困ってたバグ、枝刈りでreturnし忘れていた アホ
なんかmodify_aとmodify_bだけのほうがcとdを追加するよりいいが?なんだこれ
ミスってSolver2の方いじってREしちゃった
Solver2、最後に時間があったらいじりたい
うお~縦の山登りを入れたら12位(相対22,678,161,357点)
しかもこれミスってwidthの大きさが1で済むやつをbreakし損ねているのでもうちょっと上がる
あと縦の遷移は雑だし評価関数ももう少しいいやつがありそう
本当は縦と横を同時に焼きたいが…
改善ポイント:

  • 縦の遷移
  • 埋めるときの貪欲

本当は途中で縦線を外すみたいなことがしたいが…(さすがにそれで改善するケースはたいしてないか?)
バグを直して11位(絶対11438329点、相対24,318,699,958点)
幅を列として持ってると思ってたけどこれ集合として持ってたわ ?
多少真面目な貪欲にしたら6位(絶対11128720点、相対28,654,208,754点) すげ~
今の方針だと多分40G越えられん(涙)
でもこれで20G前後からいきなり30Gくらいまで行った人が何してるか分かった(同じ方針かは分からないが…)
縦でくっつける遷移も増やしたけど(たぶん)参加者のhighestを更新したが相対スコアは落ちるというどうすればいいのかわからない結果に
これ順位表のsnap大事かもしれん 今は6位、絶対11114353点、相対28,692,093,009点
highestを更新しているということはこの方針でも闘えるということなのかな…
上位の人が投げてくれたおかげでちょっと相対スコア上がってる ありがとうございます

9日目(3/30)

ぐわ~ さすがに横棒をいじるところで縦棒を動かしても伸びない(縦棒を動かすだけではスコアは改善しないので当然)
破壊再構築を書けということか… こんなことならちゃんとAを参照で受け取るコードにしておけばよかったぜ
いや~でも全ての日で再構築するのは厳しそう 日ごとに再構築…?
でもそうするとスコア計算がめんどくせ~~~
いやさすがに色んなものを壊しすぎなので改善しない気がする 保留で…
さらに何本か横線を端から端まで引けば固定できる場所が増えて嬉しい?
いやでもwidthの数が増えるだけか?でも固定できる幅が大きくなるのは嬉しいかも…?
いやでもその分幅は大きくなるか う~ん

10日目(3/31)

寝て起きたら順位上がってた 精神状態に良い(?)
適当に山登りをやめるのを100回解を更新しなかったらにしたけどこれがかなりいい値だったっぽい(多少伸びてびっくりした)
微妙な遷移をなくして+1/-1する遷移を+p/-pにしたら6位(絶対10980137点、相対29,575,755,727点)に再浮上
手元の評価、logの総和でいいのかよくわからん
横棒の山登りを焼きなましにしたら5位(絶対10935035点、相対32,002,220,213点)になった いいね
焼きなましの温度が壊れていたので修正したら4位(絶対10946529点、相対32,535,663,873点) OK
温度いじっただけで結構伸びるな もうちょっと頑張れるかも
start_temp=50→絶対10935035点、相対32,535,663,873点
start_temp=60→絶対10931413点、相対32,191,780,353点
start_temp=40→絶対10925410点、相対32,573,956,492点
start_temp=30→絶対10925410点、相対32,133,430,262点
もうネタ切れだ~となりながらなんだかんだで3Gくらい上げていて我ながらえらい
start_temp=45→絶対10931709点、相対31,018,474,409点(上位陣が提出したので相対スコアが変わってしまい困った)
とりあえずstart_tempは40にしておこう
ここにきて順位表がめっちゃ動く ひえ~~~
ハイパラ調整をします optuna頼んだ!

11日目(4/1)

optunaで調整したやつを投げたら下がった 悲しい(log評価が良くなかったのでは…)
山登りの評価関数だけ戻して出したら上がった optunaありがとう…(調整するハイパラをミスった感がある)
順位表動きすぎだろ 勘弁してくれ~~~
Solver2に書き足すかSolverをコピペして書き直すかどっちがいいんだ Solver2に書き足すほうが楽か…
え????? push_widthであるべきところがpush_width_aになってた ?????
でもこのバグ直したら相対スコアは下がったな そんな
ちょっと山登りをいじって6位(絶対10626353点、相対32,410,153,617点) ひ~~~
Solver2を直したけど0.3Gくらいしか上がらなかった 悲しい
よく見たら山登りの遷移がバグってる おいおい
バグ直したら相対スコア下がった はい
上位陣の提出により1Gくらい削られた 許してくれ~
コードを見直したらハイパラが間違ってた 怖
メモ:最終提出は絶対スコアが6804134のやつを出す

結果

暫定8位、最終9位で初赤perf&入黄 やった~~~

コピペすると(たぶん)どこでvectorを範囲外参照しているかがわかるソースコード(を作りたい)

※未定義動作です!!!
AtCoderのコードテストでは自分が試した範囲ではそれっぽく動きましたがわかりません…
あと健全な(未定義動作でない)使い方案も一応あります
(以下のすべての文はうまく動くということにして落書き程度に読んでください)



(自己責任で)適当にコピペ・改変してください
改善案募集中です!!!!!!!!!!!!!!

使い方(通常版)

C++20以降、もしくはC++17以降&boost(1.74以降で動くが1.79以降推奨)が必要です。
下のソースコードをincludeの後かつvectorが出てくる前に貼ってください。(includeの前に貼ってもたぶん動くようになっていると思いますが非推奨です)
includeは適当に省いてください。boost/assert/source_location.hpp以外はbits/stdc++.hに含まれています。
先に調べるソースコードコンパイルできるか確認してから貼ること、提出時は消すことを推奨します。

C++20版

#include <iostream>
#include <vector>
#include <source_location>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        subscript_and_location(int sub_,std::source_location loc_=std::source_location::current()){
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
}
namespace std{
    template<class T,class Allocator=std::allocator<T>> class vector_for_debugging:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
    namespace pmr{
        template<class T> using vector_for_debugging=std::vector_for_debugging<T,std::pmr::polymorphic_allocator<T>>;
    }
}
#define vector vector_for_debugging

boost版

#include <iostream>
#include <vector>
#include <boost/assert/source_location.hpp>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        boost::source_location loc;
        subscript_and_location(int sub_,boost::source_location loc_=BOOST_CURRENT_LOCATION){
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
}
namespace std{
    template<class T,class Allocator=std::allocator<T>> class vector_for_debugging:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
    namespace pmr{
        template<class T> using vector_for_debugging=std::vector_for_debugging<T,std::pmr::polymorphic_allocator<T>>;
    }
}
#define vector vector_for_debugging

貼ってコンパイル・実行すると、vectorを範囲外参照していた場合

ファイル名:(行:列):関数名
out of range: subscript = 添字, vector_size = vectorのサイズ

標準エラー出力に出てきてプログラムが異常終了(RE)します。
(boost版を使っている&boost1.79より前の場合多分関数名がうまく表示されません)

使用例

#include <bits/stdc++.h>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        subscript_and_location(int sub_,std::source_location loc_=std::source_location::current()){
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
}
namespace std{
    template<class T,class Allocator=std::allocator<T>> class vector_for_debugging:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
    namespace pmr{
        template<class T> using vector_for_debugging=std::vector_for_debugging<T,std::pmr::polymorphic_allocator<T>>;
    }
}
#define vector vector_for_debugging
using namespace std;
int main(){
    vector<int> v(3,0);
    cout << v[5] << endl;
}

AtCoderのコードテスト上でコンパイル・実行すると

Main.cpp:(40:16):int main()
out of range: subscript = 5, vector_size = 3

標準エラー出力に出てきて異常終了します。

長所・短所

長所

  • 貼るだけで範囲外参照を調べることができるので、デバッガを使わなくて済み楽
  • .at()と異なり、何行何列で配列外参照したかがわかる
  • .at()と異なり、もし添字がマイナスでもマイナスのままエラーに出てくる
  • 一応消し忘れても提出できる

短所

  • 未定義動作(#define vector ○○している、std名前空間にプログラムを追記している*1 )
  • 範囲外参照以外の実行時エラーがわからない
  • 消し忘れた場合実行時間が少し長くなる
  • 長い

短所が短所すぎる…

何してるの?

基本はstd::vector::operator[]に範囲チェックを追加するヘッダ - 簡潔なQと同じです。これ賢いですよね…
ここに継承コンストラクタを付けて、添字が範囲内かどうかチェックをして戻り値を[]演算子にしたものがこれです:

コード

#include <bits/stdc++.h>
using namespace std;
namespace std{
    template<class T> class vetor_for_dubugging:public vector<T>{
        using vector<T>::vector;
        public:
            T& operator[](int n){
                if(n<0||(int)this->size()<=n){
                    //範囲外参照
                    exit(EXIT_FAILURE);
                }
                return vector<T>::operator[](n);
            }
    };
}
#define vector vetor_for_dubugging

これだとどこで範囲外参照したかがわからないので、source_locationを使って[]演算子を呼び出した場所を持っておきます。
[]演算子のデフォルト引数は禁止されていて困った~~~と思っていたらc++ - operator[] caller's site source location current workaround - Stack Overflowを発見して、これを読むと構造体やクラスを引数にしてそのコンストラクタのデフォルト引数を使えばいいことが分かり賢いな~~~と思いました。
これを実装するとこうなります:
コード

#include <bits/stdc++.h>
using namespace std;
struct subscript_and_location{
    int sub;
    source_location loc;
    subscript_and_location(int sub_,source_location loc_=source_location::current()){
        sub=sub_;
        loc=loc_;
    }
};
namespace std{
    template<class T> class vetor_for_dubugging:public vector<T>{
        using vector<T>::vector;
        public:
            T& operator[](subscript_and_location n){
                if(n.sub<0||(int)this->size()<=n.sub){
                    //範囲外参照
                    clog << n.loc.file_name() << ":(" << n.loc.line() << ":" << n.loc.column() << "):" << n.loc.function_name() << endl;
                    clog << "out of range: subscript = " << n.sub << ", vector_size = " << this->size() << endl;
                    exit(EXIT_FAILURE);
                }
                return vector<T>::operator[](n.sub);
            }
    };
}
#define vector vetor_for_dubugging

後は貼った下でincludeしても大丈夫なように頑張ってstl_vector.hとかstl_bvector.hを読みながらいい感じにしています。
vector<bool>の[]演算子にはnoexceptが付いていなかったのでなんとなく場合分けしています。(なので場合分けを消しても動きます というか[[nodiscard]]とconstexprとnoexceptは消しても動きます)

健全な(未定義動作でない)使い方案

案1 デバッグ用のvectorをstd名前空間の外にすべて移して、普段はstd::vectorを使い、デバッグをしたいときだけすべてデバッグ用のvectorに書き換える

#include <iostream>
#include <vector>
#include <source_location>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        subscript_and_location(int sub_,std::source_location loc_=std::source_location::current()){
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
    template<class T,class Allocator=std::allocator<T>> class vector:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
}
int main(){
    //デバッグ時はstd::vectorをfor_debugging::vectorに書き換える
}

案2 デバッグ用のvectorをstd名前空間の外にすべて移して、普段はstd::vectorを別の名前でusing宣言したものを使い、デバッグしたい時だけusing宣言を書き換える

#include <iostream>
#include <vector>
#include <source_location>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        subscript_and_location(int sub_,std::source_location loc_=std::source_location::current()){
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
    template<class T,class Allocator=std::allocator<T>> class vector:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
}
template<class T,class Allocator=std::allocator<T>> using vec=std::vector<T,Allocator>;
//template<class T,class Allocator=std::allocator<T>> using vec=for_debugging::vector<T,Allocator>;  //デバッグ時だけこっちを使う
int main(){
    //vecをvectorとして使う
}

案3 デバッグ用のvectorをstd名前空間の外にすべて移して、デバッグ時はstd::vectorを使用するすべての関数や名前空間の先頭にusing宣言を書く(using namespace std;をしてvectorをstd::を付けずにstd::vectorを使っていないとうまくいきません)

#include <iostream>
#include <vector>
#include <source_location>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        subscript_and_location(int sub_,std::source_location loc_=std::source_location::current()){
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
    template<class T,class Allocator=std::allocator<T>> class vector:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
}
using namespace std;
int main(){
    //using for_debugging::vector;  //デバッグ時はvectorを使用する関数や名前空間の先頭にこれを書く
}

案4 使わない

おまけ1:簡易版

  • 上でusing namespace std;などをしていてvectorをstd::を付けずに使っている(std::を付けていてもstd名前空間の中に書けば大丈夫です)
  • 下でincludeしてはいけない
  • vectorが使えない
  • 戻り値を破棄していても警告が出ない
  • 自作アロケータが使えない

などの制限が付く代わりに短いバージョンです。
ほとんど『何してるの?』の2つ目のソースコードと変わりません

C++20版

#include <iostream>
#include <vector>
#include <source_location>
using namespace std;
struct sub_loc{
    int sub;
    source_location loc;
    sub_loc(int sub_,source_location loc_=source_location::current()){
        sub=sub_;
        loc=loc_;
    }
};
template<class T> struct vfd:public vector<T>{
    using vector<T>::vector;
    T& operator[](sub_loc n){
        if(n.sub<0||(int)this->size()<=n.sub){
            clog << n.loc.file_name() << ":(" << n.loc.line() << ":" << n.loc.column() << "):" << n.loc.function_name() << endl;
            clog << "out of range: subscript = " << n.sub << ", vector_size = " << this->size() << endl;
            exit(EXIT_FAILURE);
        }
        return vector<T>::operator[](n.sub);
    }
};
#define vector vfd

boost版

#include <iostream>
#include <vector>
#include <boost/assert/source_location.hpp>
using namespace std;
struct sub_loc{
    int sub;
    boost::source_location loc;
    sub_loc(int sub_,boost::source_location loc_=BOOST_CURRENT_LOCATION){
        sub=sub_;
        loc=loc_;
    }
};
template<class T> struct vfd:public vector<T>{
    using vector<T>::vector;
    T& operator[](sub_loc n){
        if(n.sub<0||(int)this->size()<=n.sub){
            clog << n.loc.file_name() << ":(" << n.loc.line() << ":" << n.loc.column() << "):" << n.loc.function_name() << endl;
            clog << "out of range: subscript = " << n.sub << ", vector_size = " << this->size() << endl;
            exit(EXIT_FAILURE);
        }
        return vector<T>::operator[](n.sub);
    }
};
#define vector vfd

おまけ2:添字の型チェック付き版

添字が整数型かどうか判定する機能がおまけで付いてくる版です。
先に調べるソースコードコンパイルできるか確認してから貼ることを推奨します。
後は通常版と同じです。

C++20版

#include <iostream>
#include <vector>
#include <source_location>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        template<class T> subscript_and_location(T sub_,std::source_location loc_=std::source_location::current()){
            if(!std::is_integral<T>::value){
                std::clog << loc_.file_name() << ":(" << loc_.line() << ":" << loc_.column() << "):" << loc_.function_name() << std::endl;
                std::clog << "subscript is not integer: subscript = " << sub_ << std::endl;
                exit(EXIT_FAILURE);
            }
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
}
namespace std{
    template<class T,class Allocator=std::allocator<T>> class vector_for_debugging:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
    namespace pmr{
        template<class T> using vector_for_debugging=std::vector_for_debugging<T,std::pmr::polymorphic_allocator<T>>;
    }
}
#define vector vector_for_debugging

boost版

#include <iostream>
#include <vector>
#include <boost/assert/source_location.hpp>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        boost::source_location loc;
        template<class T> subscript_and_location(T sub_,boost::source_location loc_=BOOST_CURRENT_LOCATION){
            if(!std::is_integral<T>::value){
                std::clog << loc_.file_name() << ":(" << loc_.line() << ":" << loc_.column() << "):" << loc_.function_name() << std::endl;
                std::clog << "subscript is not integer: subscript = " << sub_ << std::endl;
                exit(EXIT_FAILURE);
            }
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
}
namespace std{
    template<class T,class Allocator=std::allocator<T>> class vector_for_debugging:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
    namespace pmr{
        template<class T> using vector_for_debugging=std::vector_for_debugging<T,std::pmr::polymorphic_allocator<T>>;
    }
}
#define vector vector_for_debugging

貼ってコンパイル・実行すると、通常版の機能に加えて、添字が整数型ではなかった場合

ファイル名:(行:列):関数名
subscript is not integer: subscript = 添字

標準エラー出力に出てきてプログラムが異常終了(RE)します。
桁数などは指定していないため必要であれば適当にstd::clog << "subscript is not integer~の部分を改変してください(すみません)

使用例

#include <bits/stdc++.h>
namespace for_debugging{
    struct subscript_and_location{
        int sub;
        std::source_location loc;
        template<class T> subscript_and_location(T sub_,std::source_location loc_=std::source_location::current()){
            if(!std::is_integral<T>::value){
                std::clog << loc_.file_name() << ":(" << loc_.line() << ":" << loc_.column() << "):" << loc_.function_name() << std::endl;
                std::clog << "subscript is not integer: subscript = " << sub_ << std::endl;
                exit(EXIT_FAILURE);
            }
            sub=sub_;
            loc=loc_;
        }
        void check_out_of_range(size_t sz){
            if(sub<0||(int)sz<=sub){
                std::clog << loc.file_name() << ":(" << loc.line() << ":" << loc.column() << "):" << loc.function_name() << std::endl;
                std::clog << "out of range: subscript = " << sub << ", vector_size = " << sz << std::endl;
                exit(EXIT_FAILURE);
            }
        }
    };
}
namespace std{
    template<class T,class Allocator=std::allocator<T>> class vector_for_debugging:public std::vector<T,Allocator>{
        using std::vector<T,Allocator>::vector;
        public:
            [[nodiscard]] constexpr std::vector<T,Allocator>::reference operator[](for_debugging::subscript_and_location n) noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
            [[nodiscard]] constexpr std::vector<T,Allocator>::const_reference operator[](for_debugging::subscript_and_location n) const noexcept(!std::is_same<T,bool>::value){
                n.check_out_of_range(this->size());
                return std::vector<T,Allocator>::operator[](n.sub);
            }
    };
    namespace pmr{
        template<class T> using vector_for_debugging=std::vector_for_debugging<T,std::pmr::polymorphic_allocator<T>>;
    }
}
#define vector vector_for_debugging
using namespace std;
int main(){
    vector<int> v(3,0);
    cout << v[1.4] << endl;
}

AtCoderのコードテスト上でコンパイル・実行すると

Main.cpp:(45:18):int main()
subscript is not integer: subscript = 1.4

標準エラー出力に出てきて異常終了します。

雑談

無限時間溶かしたミス集:

  • テンプレートの部分特殊化にも一次テンプレートのデフォルトテンプレート引数が引き継がれる

例:

template<class T,class U=int> class c{
};
template<class U> class c<bool,U>{
};

これをc<bool>で使うとUがintになる

  • typedef宣言も継承できる

  • #defne vector ○○は未定義

みんなも無限時間溶かさないように気を付けよう!

*1:えびちゃんさんからご指摘をいただきました(https://twitter.com/rsk0315_h4x/status/1727637308336435238) ありがとうございます!

色々な解説を読んでもナップザックDPがわからなかった競プロer向けにナップザックDPを解説する

非常に長い解説になっています メモ化再帰は使いません

1.はじめに

  • 非常に長いので先にもっと簡単でわかりやすく短い解説を探して読んだほうが良いです
  • 「1次元DPならギリギリ理解できるような気がするけどナップザックDPはマジで謎!!!」みたいな競プロer向けの記事です
  • 競プロer向けなのでソースコードは適当です(特にPythonは競プロer向けだとしても滅茶苦茶な書き方の可能性があります すみません!!!)

2.前提知識(知ってたら飛ばしてOK)

C++er向け

  • C++の基本的な文法(APG4bの3.03くらいまで)
  • mapの存在しないKeyへのアクセス

Valueがintやlong longのとき、まだ存在しないKeyに[]でアクセスした場合、Valueが0である要素が新しく作られます。*1
例えば、

#include <bits/stdc++.h>
using namespace std;
int main(){
    map<int,int> mp;
    cout << mp[10] << endl;
    mp[100]+=5;
    cout << mp[100] << endl;
}

を実行すると

0
5

が出力されます。

  • 範囲for文

配列の中身をすべて舐めることができます ぺろぺろ
autoを使うことでいちいちvectorを宣言するときに使った型を書かなくて済みます。
例えば、

#include <bits/stdc++.h>
using namespace std;
int main(){
    vector<int> v={10,20,30};
    for(auto i:v){
        cout << i << endl;
    }
    vector<pair<int,int>> vp={{3,6},{4,8},{5,10000}};
    for(auto i:vp){
        cout << i.first << " " << i.second << endl;
    }
}

を実行すると

10
20
30
3 6
4 8
5 10000

が出力されます。(参照をつけたほうが嬉しいときもあるけど説明が面倒なので省略!)
pairの方は構造化束縛(autoでいい感じに受け取れる機能)というのを使うと

#include <bits/stdc++.h>
using namespace std;
int main(){
    vector<pair<int,int>> vp={{3,6},{4,8},{5,10000}};
    for(auto [k,v]:vp){
        cout << k << " " << v << endl;
    }
}

と書くことができます。
mapでも同じことができます。(中身がpair<Key,Value>なのに注意!) 例えば

#include <bits/stdc++.h>
using namespace std;
int main(){
    map<int,int> mp;
    mp[3]=6;
    mp[4]=8;
    mp[5]=10000;
    for(auto [k,v]:mp){
        cout << k << " " << v << endl;
    }
}

と書くことができます。楽でよいですね。

Pythonista向け

  • Pythonの基本的な文法(ABCのBが解けるかつdictが使えるくらい?基準がよくわかりません、すみません… 知らない文法が出てきたら適宜検索してください)
  • defaultdict

dictでKeyが存在するかどうかの分岐を書くのが面倒なのでこの記事ではdefaultdictを使います。
例えば、

from collections import defaultdict
d=defaultdict(int)  #最初にValueの型を指定することで初期値を決めることができる
d[3]=5
d[4]+=8
d[5]=max(d[5],10000)
for k,v in d.items():  #items()にアクセスすることでうまく動く
    print(k,v)

を実行すると

3 5
4 8
5 10000

が出力されます。
最初にintを渡しておいて*2存在しないKeyにアクセスするとValueが0の要素が新しく作られます。*3
+=とかmaxとかを何も考えずに使えてよいですね。

3.ナップザックDPを解く

とりあえず、この記事の目標をEDPC-Dを解くことにします。(問題はリンク先で確認してください)
(以下、C++ソースコードGCCPythonソースコードはPyPy3で提出することとします)

3-1.全探索

いきなりDPだ!!!としてもよくわからないので、とりあえず全探索を考えましょう。 まず、品物の選び方は何通りでしょうか。品物iについて考えると、iを選ぶ場合と選ばない場合の2通りがあります。品物は全部でN個あるので、選び方は2^{N}通りあります。*4
こういうのはbit全探索だ!!!と思ってる人もいると思うんですが、とりあえず品物iを選ぶ、選ばないの2通りを前から考えていく実装をすることにします。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<pair<int,long long>> s;  //あり得るpair<重さの総和,価値の総和>を全部sに突っ込む
    s.push_back({0,0});  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        vector<pair<int,long long>> new_s;  //新しいsを用意
        for(auto [sum_w,sum_v]:s){
            new_s.push_back({sum_w,sum_v});  //品物iを取らない場合
            new_s.push_back({sum_w+w[i],sum_v+v[i]});  //品物iを取る場合
        }
        s=new_s;  //sを更新
    }
    long long ans=0;
    for(auto [sum_w,sum_v]:s){
        if(sum_w<=W){
            ans=max(ans,sum_v);  //最終的な重さの総和がW以下のものでansを更新
        }
    }
    cout << ans << endl;
}

C++er向け補足:重さの総和が最大なのはすべての品物を選び、かつすべての品物の重さが最大のときなので、100\times 10^5=10^7となりintに収まり、重さの総和が最大なのはすべての品物を選び、かつすべての品物の重さが最大のときなので、最大で100\times 10^9=10^{11}となりintには収まらずlong longに収まります。

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
s=[]  #あり得る(重さの総和,価値の総和)を全部sに突っ込む
s.append((0,0))  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    new_s=[]  #新しいsを用意
    for sum_w,sum_v in s:
        new_s.append((sum_w,sum_v))  #品物iを取らない場合
        new_s.append((sum_w+w[i],sum_v+v[i]))  #品物iを取る場合
    s=new_s[:]  #sを更新
ans=0
for sum_w,sum_v in s:
    if sum_w<=W:
        ans=max(ans,sum_v)  #最終的な重さの総和がW以下のものでansを更新
print(ans)


(以下、品物の添字を0-indexed*5とします)
実装方針としては、あり得る(重さの総和,価値の総和)のペアを配列s*6で持って、どんどん更新していく感じです。
品物iを取らない場合、(重さの総和,価値の総和)は変化しません。
品物iを取る場合、(重さの総和,価値の総和)は(重さの総和+w_i,価値の総和+v_i)に変化します。
(ちなみに、このように、AからBへ更新していくことを「遷移する」といいます。*7 
「遷移」という言葉は使いやすいので覚えておくとよいです。*8
AからBへ更新していくときのAを遷移元、Bを遷移先と呼ぶことにします。
) 最終的に、品物の選び方がすべてsに入っているので、重さの総和がW以下である選び方のうち、価値の総和が最大であるものを出力します。
(sを更新するのがよくわからない人は次の実装例も見ながら読んでみてください)
これを提出すると、TLEになります。

さっき品物の選び方は2^{N}通りと書きました。ということは、new_sに値を突っ込む回数が2^{N}回になっています。N=100のとき2^{N}=1267650600228229401496703205376となり、これは余裕でTLEします。
ここで、少し実装を変えます。sをnew_sで更新するのも良いのですが、いわゆるナップザックDPの形に近づけます。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<vector<pair<int,long long>>> s(N+1);
    //品物iよりも前の品物の選び方であり得るpair<重さの総和,価値の総和>を全部s[i]に突っ込む
    s[0].push_back({0,0});  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(auto [sum_w,sum_v]:s[i]){
            s[i+1].push_back({sum_w,sum_v});  //品物iを取らない場合
            s[i+1].push_back({sum_w+w[i],sum_v+v[i]});  //品物iを取る場合
        }
    }
    long long ans=0;
    for(auto [sum_w,sum_v]:s[N]){  //最後はs[N]
        if(sum_w<=W){
            ans=max(ans,sum_v);  //最終的な重さの総和がW以下のものでansを更新
        }
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
s=[[] for i in range(N+1)]
#品物iよりも前の品物の選び方であり得る(重さの総和,価値の総和)を全部s[i]に突っ込む
s[0].append((0,0))  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for sum_w,sum_v in s[i]:
        s[i+1].append((sum_w,sum_v))  #品物iを取らない場合
        s[i+1].append((sum_w+w[i],sum_v+v[i]))  #品物iを取る場合
ans=0
for sum_w,sum_v in s[N]:  #最後はs[N]
    if sum_w<=W:
        ans=max(ans,sum_v)  #最終的な重さの総和がW以下のものでansを更新
print(ans)


やっていることはあまり変わりません。new_sを用意したりsを更新したりするのが面倒なので、最初に可変長配列をN+1個用意しておいて、sからnew_sではなくs[i]からs[i+1]に対して更新しているだけです。
このとき、s[i]は品物iよりも前の品物の選び方であり得る(品物の重さの総和,品物の価値の総和)が入っています。半開区間[0,i)*9に含まれる品物の選び方と捉えることもできます。
最終的に、品物の選び方がすべてs[N]に入ります。(品物Nは存在しませんが、品物0から品物N-1までの選び方が入っていると考えることができます)

3-2.工夫①(範囲の工夫)

ここから、どんどん探索を改善していきます。
まず、答えとしてあり得る選び方は重さの総和がW以下なので、重さの総和がWを超えるような選び方を持っておく必要はありません。これを実装すると、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<vector<pair<int,long long>>> s(N+1);
    //品物[0,i)の選び方で答えとしてあり得るpair<重さの総和,価値の総和>を全部s[i]に突っ込む
    s[0].push_back({0,0});  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(auto [sum_w,sum_v]:s[i]){
            s[i+1].push_back({sum_w,sum_v});  //品物iを取らない場合
            if(sum_w+w[i]<=W){
                s[i+1].push_back({sum_w+w[i],sum_v+v[i]});
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(auto [sum_w,sum_v]:s[N]){  //最後はs[N]
        ans=max(ans,sum_v);  //ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
s=[[] for i in range(N+1)]
#品物[0,i)の選び方で答えとしてあり得る(重さの総和,価値の総和)を全部s[i]に突っ込む
s[0].append((0,0))  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for sum_w,sum_v in s[i]:
        s[i+1].append((sum_w,sum_v))  #品物iを取らない場合
        if sum_w+w[i]<=W:
            s[i+1].append((sum_w+w[i],sum_v+v[i]))  #品物iを取る場合(重さの総和がW以下)
ans=0
for sum_w,sum_v in s[N]:  #最後はs[N]    
    ans=max(ans,sum_v)  #ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
print(ans)


こうなります。品物iを選ばない場合はsum_wが増えないのでW以下か調べなくてOKです。
これを提出すると、TLEになります。

よく考えると、Wが大きいかつv_iが小さいときは、あまりこの工夫は効きません。例えば、N=100W=100000v_iがすべて1のとき、どんな選び方でも重さの総和は100以下なので、すべての更新がif文を貫通します。

3-3.工夫②(持つ状態の工夫)

ここで、一度sの中身を見てみましょう。

3 100
2 3
2 4
4 4

のような入力でのsの中身を出力してみます。
(↓これは適当にsの中身を出力するソースコードなので真面目に読まなくてよいです)

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<vector<pair<int,long long>>> s(N+1);
    //品物[0,i)の選び方で答えとしてあり得るpair<重さの総和,価値の総和>を全部s[i]に突っ込む
    s[0].push_back({0,0});  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(auto [sum_w,sum_v]:s[i]){
            s[i+1].push_back({sum_w,sum_v});  //品物iを取らない場合
            if(sum_w+w[i]<=W){
                s[i+1].push_back({sum_w+w[i],sum_v+v[i]});
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    
    //i:(重さの総和,価値の総和)…を出力する
    for(int i=0;i<=N;i++){
        cout << i << ":";
        for(auto [sum_w,sum_v]:s[i]){
            cout << "(" << sum_w << "," << sum_v << ") ";
        }
        cout << endl;
    }
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
s=[[] for i in range(N+1)]
#品物[0,i)の選び方で答えとしてあり得る(重さの総和,価値の総和)を全部s[i]に突っ込む
s[0].append((0,0))  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for sum_w,sum_v in s[i]:
        s[i+1].append((sum_w,sum_v))  #品物iを取らない場合
        if sum_w+w[i]<=W:
            s[i+1].append((sum_w+w[i],sum_v+v[i]))  #品物iを取る場合(重さの総和がW以下)
for i in range(N+1):
    print(i,end=":")
    for sum_w,sum_v in s[i]:
        print("("+str(sum_w)+","+str(sum_v)+")",end=" ")
    print()

Pythonista向け補足:C++に寄せて書こうとしたら大変なことになってしまいました 実際にsの中身を見たいときはprint(s)とかでよいと思います


すると、

0:(0,0) 
1:(0,0) (2,3) 
2:(0,0) (2,4) (2,3) (4,7) 
3:(0,0) (4,4) (2,4) (6,8) (2,3) (6,7) (4,7) (8,11) 

のような出力になります。(i:(x,y)…はs[i]に入っているペアが(重さの総和がx,価値の総和がy)…であることを表しています)
よく見ると、(4,4)と(4,7)、(2,4)と(2,3)のように、重さが同じで価値のみが異なるペアが入っています。
さて、本当に(4,4)や(2,3)は必要でしょうか。
重さの総和が同じなら、価値の総和が大きい方がお得です。
つまり、s[i]には、同じ重さならば、一番価値の総和が大きいペアしか必要ありません。
一応雑に証明してみます。(読み飛ばしてもOK)

雑証明 (重さと価値に「の総和」を付けるとごちゃごちゃしたのでここだけ外れています すみません)

(w,v)から遷移して最終的にs[N]に入っている、品物の選び方としてあり得る(重さ,価値)を(w,v)から遷移した解とする。
(w,v)から遷移した解のうち、一番価値が高いものを(w,v)から遷移した最適解とする。

命題:
s[i]に(重さ,価値)が(a,b),(a,c)(b\leqq c)である2つが入っていた場合、(a,b)から遷移した最適解の価値は、(a,c)から遷移した最適解の価値を超えない。

証明:
(a,b)から遷移した最適解を(x,y)とする。この最適解の価値はyである。
(a,b)から(x,y)に遷移しているので、重さがx-a、価値がy-b増えるような遷移が存在する。
ここで、x\leqq Wよりa+(x-a)\leqq Wなので(a,c)から同じ遷移をさせることができ、(a+(x-a),c+(y-b))=(x,y+(c-b))(a,c)から遷移した解として存在する。この解の価値はy+(c-b)である。
b\leqq cより、0\leqq c-bなので、y\leqq y+(c-b)となり、(a,b)から遷移した最適解の価値が(a,c)から遷移した解の価値を超えない。
よって、(a,b)から遷移した最適解の価値は、(a,c)から遷移した最適解の価値を超えない。Q.E.D.
(思ったより長くなってしまった… コンテスト中は同じ重さなら価値が高い方が嬉しいよね~くらいでよいです)

結局、s[i]には、同じ重さならば、一番価値が大きいペアしか必要ないので、c++ならmap、Pythonならdefaultdictなどを使って(重さの総和,その重さの総和のものの価値の総和の最大値)のようなペアを作るように実装してみます。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<map<int,long long>> s(N+1);
    //品物[0,i)の選び方の、map<重さの総和,その重さの総和のものの価値の総和の最大値)>をs[i]とする
    s[0][0]=0;  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(auto [sum_w,sum_v]:s[i]){
            s[i+1][sum_w]=max(s[i+1][sum_w],sum_v);  //品物iを取らない場合
            if(sum_w+w[i]<=W){
                s[i+1][sum_w+w[i]]=max(s[i+1][sum_w+w[i]],sum_v+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(auto [sum_w,sum_v]:s[N]){  //最後はs[N]
        ans=max(ans,sum_v);  //ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

from collections import defaultdict
N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
s=[defaultdict(int) for i in range(N+1)]
#品物[0,i)の選び方の、{重さの総和:その重さの総和のものの価値の総和の最大値}をs[i]とする
s[0][0]=0  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for sum_w,sum_v in s[i].items():
        s[i+1][sum_w]=max(s[i+1][sum_w],sum_v)  #品物iを取らない場合
        if sum_w+w[i]<=W:
            s[i+1][sum_w+w[i]]=max(s[i+1][sum_w+w[i]],sum_v+v[i])
            #品物iを取る場合(重さの総和がW以下)
ans=0
for sum_w,sum_v in s[N].items():  #最後はs[N]
    ans=max(ans,sum_v)  #ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
print(ans)


ナップザックDPっぽくなってきましたね!!!
s[i][sum_w]=品物iよりも前の品物の選び方で重さの総和がsum_wのものの価値の総和の最大値としています。
品物iを取らない、取るどちらの遷移もそれまでの価値の最大値を超える場合に行うとすると、価値の最大値がいい感じに求まります。
これを提出すると、C++なら(たぶん)TLEします。そんな…
Pythonならギリギリで通るかもしれません。自分は通りました。(Submission #43903387 - Educational DP Contest)
すみません、実は最初は計算量評価を間違えていて割と余裕でACすると思っていたので←は??? このあたりの文章が怪しくなっています

この実装は、品物iよりも前の品物の選び方の、(重さの総和,その重さの総和のものの価値の総和の最大値)が突っ込まれたものをs[i]としていて、品物の重さはWを超えないように実装しているので、s[i]の要素数Wを超えません。これがN個あり、s[i]の要素一つ一つに対して
(a)それぞれの要素に対して
(b)遷移できるかどうか調べています。

計算量は、C++なら、(a)はO(\log W)、s[i+1]の要素もW個以下なので(b)もO(\log W)となり*10、s[i]一つにつきO(W(\log W)^2)であり、これをN回行うので、全体でO(NW(\log W)^2)となり、これはN=100W=100000だと実行時間制限である2secに間に合わなさそうです。(参考:100\times 10^5\times (\log_2 10^5)^2\fallingdotseq 2758801567)

Pythonなら(a)も(b)も平均*11O(1)*12なので、s[i]一つにつき平均O(W)、全体で平均O(NW)となり、これはN=100W=100000でも実行時間制限である2secに間に合いそうですが、(参考:100\times 10^5=10^7)平均計算量なので、ギリギリでACしたりTLEしたりします。

ここで、s[i]に入っている重みがWを超えない、つまり0以上W以下なので、C++のmapやPythonのdefaultdictではなくただの大きさW+1の配列を使って実装することができます。*13
ただし、そのまま配列を使ってsum_wを0からWまで動かすと存在しないはずの(重さの総和,価値の総和)から遷移してしまう*14ので、うまく回避しなければいけません。
実装方針としてはおおよそ2通りあり、1つ目は存在しない遷移をさせない、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<vector<long long>> s(N+1,vector<long long>(W+1,-1));
    //s[i][sum_w]=品物[0,i)の選び方で重さの総和がsum_wのものの価値の総和の最大値とする
    //存在しない遷移元に関しては-1で初期化しておいて、遷移しないようにする
    s[0][0]=0;  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(int sum_w=0;sum_w<=W;sum_w++){
            long long sum_v=s[i][sum_w];  //sum_vはs[i][sum_w]
            if(sum_v==-1) continue;  //存在しない遷移元から遷移しないようにする
            s[i+1][sum_w]=max(s[i+1][sum_w],sum_v);  //品物iを取らない場合
            if(sum_w+w[i]<=W){
                s[i+1][sum_w+w[i]]=max(s[i+1][sum_w+w[i]],sum_v+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(int sum_w=0;sum_w<=W;sum_w++){  //最後はs[N]
        long long sum_v=s[N][sum_w];
        ans=max(ans,sum_v);  //ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
s=[[-1]*(W+1) for i in range(N+1)]
#s[i][sum_w]=品物[0,i)の選び方で重さの総和がsum_wのものの価値の総和の最大値とする
#存在しない遷移元に関しては-1で初期化しておいて、遷移しないようにする
s[0][0]=0  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for sum_w in range(W+1):
        sum_v=s[i][sum_w]  #sum_vはs[i][sum_w]
        if sum_v==-1:
            continue  #存在しない遷移元から遷移しないようにする
        s[i+1][sum_w]=max(s[i+1][sum_w],sum_v)  #品物iを取らない場合
        if sum_w+w[i]<=W:
            s[i+1][sum_w+w[i]]=max(s[i+1][sum_w+w[i]],sum_v+v[i])
            #品物iを取る場合(重さの総和がW以下)
ans=0
for sum_w in range(W+1):  #最後はs[N]
    sum_v=s[N][sum_w]
    ans=max(ans,sum_v)  #ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
print(ans)


とする実装です。
sを-1で初期化しておくことで、存在しない遷移元(例えば(0,1)など)から遷移しないようにcontinueさせることができます。価値の総和は負にならないので、遷移先でmaxを取るとmax(-1,価値の総和)=価値の総和となり、うまく動きます。
もちろん-1ではなく価値の総和の最小値である0より小さい数であればどれを使って実装してもよいです。
2つ目は存在しない遷移をしても答えに影響しない、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    long long INF=1e18;
    vector<vector<long long>> s(N+1,vector<long long>(W+1,-INF));
    //s[i][sum_w]=品物[0,i)の選び方で重さの総和がsum_wのものの価値の総和の最大値とする
    //存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
    s[0][0]=0;     //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(int sum_w=0;sum_w<=W;sum_w++){
            long long sum_v=s[i][sum_w];  //sum_vはs[i][sum_w]
            s[i+1][sum_w]=max(s[i+1][sum_w],sum_v);  //品物iを取らない場合
            if(sum_w+w[i]<=W){
                s[i+1][sum_w+w[i]]=max(s[i+1][sum_w+w[i]],sum_v+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(int sum_w=0;sum_w<=W;sum_w++){  //最後はs[N]
        long long sum_v=s[N][sum_w];
        ans=max(ans,sum_v);  //ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
INF=10**18
s=[[-INF]*(W+1) for i in range(N+1)]
#s[i][sum_w]=品物[0,i)の選び方で重さの総和がsum_wのものの価値の総和の最大値とする
#存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
s[0][0]=0  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for sum_w in range(W+1):
        sum_v=s[i][sum_w]  #sum_vはs[i][sum_w]
        s[i+1][sum_w]=max(s[i+1][sum_w],sum_v)  #品物iを取らない場合
        if sum_w+w[i]<=W:
            s[i+1][sum_w+w[i]]=max(s[i+1][sum_w+w[i]],sum_v+v[i])
            #品物iを取る場合(重さの総和がW以下)
ans=0
for sum_w in range(W+1):  #最後はs[N]
    sum_v=s[N][sum_w]
    ans=max(ans,sum_v)  #ansを更新(必ずsum_wはW以下なので分岐を書かなくてOK)
print(ans)


とする実装です。
この問題の答えは必ず0以上になる*15ので、最初にいくら価値を足しても答えになり得ない値(Nの上限が100v_iの上限が10^9なので、100\times 10^9\times -1=-10^{11}よりも小さい値)で配列を初期化してあげると、いちいちcontinueしなくても余計な遷移を考えなくて済みます。ここではINF=10^{18}として-INFで初期化をしています。
maxを取るくだりは1つ目の実装と同じようにmax(-INF,価値の総和)=価値の総和となり、うまく動きます。
(実はこの問題の場合初期化の値は0以下であれば大丈夫なのですが、その話は後で…)
どちらの実装でも、C++Python関係なく計算量はO(NW)になります。
これを提出すると、無事ACすることができます!!!やった~~~~~~~~

2つ目の実装はcontinueを書かなくて済み楽なので、この後もこの2つ目のような実装をすることにします。
結局、s[i][j]=「品物iよりも前の品物の選び方で重さの総和がjのものの価値の総和の最大値」なので、sum_wをjにして、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    long long INF=1e18;
    vector<vector<long long>> s(N+1,vector<long long>(W+1,-INF));
    //s[i][j]=品物[0,i)の選び方で重さの総和がjのものの価値の総和の最大値とする
    //存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
    s[0][0]=0;     //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            long long sum_v=s[i][j];  //sum_vはs[i][j]
            s[i+1][j]=max(s[i+1][j],sum_v);  //品物iを取らない場合
            if(j+w[i]<=W){
                s[i+1][j+w[i]]=max(s[i+1][j+w[i]],sum_v+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(int j=0;j<=W;j++){  //最後はs[N]
        long long sum_v=s[N][j];
        ans=max(ans,sum_v);  //ansを更新(必ずjはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
INF=10**18
s=[[-INF]*(W+1) for i in range(N+1)]
#s[i][j]=品物[0,i)の選び方で重さの総和がjのものの価値の総和の最大値とする
#存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
s[0][0]=0  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for j in range(W+1):
        sum_v=s[i][j]  #sum_vはs[i][j]
        s[i+1][j]=max(s[i+1][j],sum_v)  #品物iを取らない場合
        if j+w[i]<=W:
            s[i+1][j+w[i]]=max(s[i+1][j+w[i]],sum_v+v[i])
            #品物iを取る場合(重さの総和がW以下)
ans=0
for j in range(W+1):  #最後はs[N]
    sum_v=s[N][j]
    ans=max(ans,sum_v)  #ansを更新(必ずjはW以下なので分岐を書かなくてOK)
print(ans)


sum_vをいちいち作らずそのままアクセスして、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    long long INF=1e18;
    vector<vector<long long>> s(N+1,vector<long long>(W+1,-INF));
    //s[i][j]=品物[0,i)の選び方で重さの総和がjのものの価値の総和の最大値とする
    //存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
    s[0][0]=0;  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            s[i+1][j]=max(s[i+1][j],s[i][j]);  //品物iを取らない場合
            if(j+w[i]<=W){
                s[i+1][j+w[i]]=max(s[i+1][j+w[i]],s[i][j]+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(int j=0;j<=W;j++){  //最後はs[N]
        ans=max(ans,s[N][j]);  //ansを更新(必ずjはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
INF=10**18
s=[[-INF]*(W+1) for i in range(N+1)]
#s[i][j]=品物[0,i)の選び方で重さの総和がjのものの価値の総和の最大値とする
#存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
s[0][0]=0  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for j in range(W+1):
        s[i+1][j]=max(s[i+1][j],s[i][j])  #品物iを取らない場合
        if j+w[i]<=W:
            s[i+1][j+w[i]]=max(s[i+1][j+w[i]],s[i][j]+v[i])
            #品物iを取る場合(重さの総和がW以下)
ans=0
for j in range(W+1):  #最後はs[N]
    ans=max(ans,s[N][j])  #ansを更新(必ずjはW以下なので分岐を書かなくてOK)
print(ans)


変数名をsからdpにすると、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    long long INF=1e18;
    vector<vector<long long>> dp(N+1,vector<long long>(W+1,-INF));
    //dp[i][j]=品物[0,i)の選び方で重さの総和がjのものの価値の総和の最大値とする
    //存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
    dp[0][0]=0;  //最初はどの品物も選んでいないので重さの総和も価値の総和も0
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            dp[i+1][j]=max(dp[i+1][j],dp[i][j]);  //品物iを取らない場合
            if(j+w[i]<=W){
                dp[i+1][j+w[i]]=max(dp[i+1][j+w[i]],dp[i][j]+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    long long ans=0;
    for(int j=0;j<=W;j++){  //最後はdp[N]
        ans=max(ans,dp[N][j]);  //ansを更新(必ずjはW以下なので分岐を書かなくてOK)
    }
    cout << ans << endl;
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
INF=10**18
dp=[[-INF]*(W+1) for i in range(N+1)]
#dp[i][j]=品物[0,i)の選び方で重さの総和がjのものの価値の総和の最大値とする
#存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
dp[0][0]=0  #最初はどの品物も選んでいないので重さの総和も価値の総和も0
for i in range(N):
    for j in range(W+1):
        dp[i+1][j]=max(dp[i+1][j],dp[i][j])  #品物iを取らない場合
        if j+w[i]<=W:
            dp[i+1][j+w[i]]=max(dp[i+1][j+w[i]],dp[i][j]+v[i])
            #品物iを取る場合(重さの総和がW以下)
ans=0
for j in range(W+1):  #最後はdp[N]
    ans=max(ans,dp[N][j])  #ansを更新(必ずjはW以下なので分岐を書かなくてOK)
print(ans)


となり、見たことのあるナップザックDPの実装になりました!!!おめでとうございます!!!

3-4.おまけ?

さて、さっき「見たことのあるナップザックDPの実装になりました」と書きましたが、このタイプの実装ではない、最後がdp[N][W]を出力するだけの実装を見たことがある人もいるのではないでしょうか。
この場合、dp[i][j]=「品物iよりも前の品物の選び方で重さの総和がj以下のものの価値の総和の最大値」として考えたくなります。
この考え方をすると、dp[0][j]はすべて0で初期化したほうがよさそうです。
dpを定義通りに更新すると、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    long long INF=1e18;
    vector<vector<long long>> dp(N+1,vector<long long>(W+1,-INF));
    //dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
    //存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えになり得ないようにする
    for(int j=0;j<=W;j++){
        dp[0][j]=0;
        //最初はどの品物も選んでいないので重さの総和がj以下のものの価値の総和の最大値は0
    }
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            for(int k=j;k<=W;k++){
                dp[i+1][k]=max(dp[i+1][k],dp[i][j]);  //品物iを取らない場合
            }
            for(int k=j+w[i];k<=W;k++){
                dp[i+1][k]=max(dp[i+1][k],dp[i][j]+v[i]);  //品物iを取る場合
            }
        }
    }
    cout << dp[N][W] << endl;
    //答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
INF=10**18
dp=[[-INF]*(W+1) for i in range(N+1)]
#dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
#存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
for j in range(W+1):
    dp[0][j]=0  #最初はどの品物も選んでいないので重さの総和がj以下のものの価値の総和の最大値は0
for i in range(N):
    for j in range(W+1):
        for k in range(j,W+1):
            dp[i+1][k]=max(dp[i+1][k],dp[i][j])  #品物iを取らない場合
        for k in range(j+w[i],W+1):
            dp[i+1][k]=max(dp[i+1][k],dp[i][j]+v[i])
            #品物iを取る場合(重さの総和がW以下)
print(dp[N][W])  #答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値


となります。
dp[i][j]=「品物iよりも前の品物の選び方で重さの総和がj以下のものの価値の総和の最大値」なので、dp[i][j]から品物iを取る取らないどちらかを選んで、重さの総和がXになったとき、重さの総和がX以上であるすべてのkに対して、dp[i][j]からdp[i+1][k]への遷移をしなければいけません。
このDPはたしかに正しい答えを出力しますが、このままではO(NW^2)*16になってしまい、TLEしてしまいます。(参考:N=100W=10^5のときNW^2=10^{12})
実は、3-3の最後の実装のように、dp[i][j]からは取る、取らないそれぞれ1回ずつの遷移だけで大丈夫です。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    long long INF=1e18;
    vector<vector<long long>> dp(N+1,vector<long long>(W+1,-INF));
    //dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
    //存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えになり得ないようにする
    for(int j=0;j<=W;j++){
        dp[0][j]=0;
        //最初はどの品物も選んでいないので重さの総和がj以下のものの価値の総和の最大値は0
    }
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            dp[i+1][j]=max(dp[i+1][j],dp[i][j]);  //品物iを取らない場合
            if(j+w[i]<=W){
                dp[i+1][j+w[i]]=max(dp[i+1][j+w[i]],dp[i][j]+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    cout << dp[N][W] << endl;
    //答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
INF=10**18
dp=[[-INF]*(W+1) for i in range(N+1)]
#dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
#存在しない遷移元に関しては-INFで初期化しておいて、遷移しても答えに影響しないようにする
for j in range(W+1):
    dp[0][j]=0  #最初はどの品物も選んでいないので重さの総和がj以下のものの価値の総和の最大値は0
for i in range(N):
    for j in range(W+1):
        dp[i+1][j]=max(dp[i+1][j],dp[i][j])  #品物iを取らない場合
        if j+w[i]<=W:
            dp[i+1][j+w[i]]=max(dp[i+1][j+w[i]],dp[i][j]+v[i])
            #品物iを取る場合(重さの総和がW以下)
print(dp[N][W])  #答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値


なぜこの実装でうまくDPが回るのでしょうか?とりあえず答えだけ考えてみましょう。
(どっちがどっちのDPかわからなくなるので、3-3の最後のようなDPの変数名をs、今書いたdp[N][W]を出力するDPの変数名をdpとします)
3-3のようなDPでは、最後にs[N][j](0\leqq j\leqq W)を全探索して最大の値を持ってきていました。
ここで、最大の値が入った場所をs[N][x]とします。
このとき、s[0][0]からいい感じに遷移をしてs[N][x]に値が入っているはずです。
今書いたDPは、最初にdp[0][j](0\leqq j\leqq W)0で初期化しました。ということは、dp[0][W-x]*17からsと同じような遷移をすると、dp[N][W-x+x]、つまりdp[N][W]s[N][x]の値が入っているはずです。
s[N][x]は最大の値だったので、maxを取っても別の値になることはありません。
よって、このDPでdp[N][W]を答えとしても正しいことがわかりました。

とりあえず、答えは正しいことがわかりました。ではすべてのdp[i][j]は、本当にdpの定義、つまり「品物iよりも前の品物の選び方で重さの総和がj以下のものの価値の総和の最大値」になっているのでしょうか。
せっかくなのでまた雑証明をしたいと思います。(読み飛ばしてもOK)

雑証明 (前提として、3-4の最初に書いたTLEするDPが定義を満たすとしています 雑なので許して~)
補題
広義単調増加である長さNの数列A,Bがあり、C_i=max(A_i,B_i)(0\leqq i\lt N)と定義される数列Cがある。
このとき、Cは広義単調増加である。
証明:
C_i=max(A_i,B_i)なのでC_iA_iB_iである。
C_i=A_iのとき、A_i\leqq A_{i+1}であるので、C_i\leqq max(A_{i+1},B_{i+1})=C_{i+1}である。
C_i=B_iのとき、B_i\leqq B_{i+1}であるので、C_i\leqq max(A_{i+1},B_{i+1})=C_{i+1}である。
以上から、C_iA_iB_iどちらでもC_i\leqq C_{i+1}が成り立つ。よってCは広義単調増加である。

命題:
dp[i+1][k]=max(dp[i+1][k],dp[i][j])(j+1\leqq k\leqq W)
dp[i+1][k]=max(dp[i+1][k],dp[i][j]+v[i])(j+w[i]+1\leqq k\leqq W)
どちらの遷移もする必要がない。

証明:
まず、数学的帰納法を用いてdp[i](0\leqq i\leqq N)はすべて広義単調増加であることを示す。
(1)
i=0のとき、dp[0][j](0\leqq j\leqq W)0で初期化されているので、dp[i]は広義単調増加である。
(2)
dp[i]が広義単調増加であると仮定する。

0\leqq j\lt w[i]としてdp[i+1][j]を考える。
w[i]\leqq j+w[i]より、
dp[i+1][j]=max(dp[i+1][j],dp[i][j])の遷移しか存在しない。
dp[i+1][j]の初期値は-10^{18}なので、
dp[i+1][j]=max(-10^{18},dp[i][j])
つまり
dp[i+1][j]=dp[i][j]である。
dp[i]は広義単調増加より、dp[i+1][j](0\leqq j\lt w[i])の範囲において広義単調増加である。

w[i]\leqq j\leqq Wとしてdp[i+1][j]を考える。
dp[i+1][j]=max(dp[i+1][j],dp[i][j])
dp[i+1][j]=max(dp[i+1][j],dp[i][j-w[i]]+v[i])
の2つの遷移が存在する。
dp[i+1][j]の初期値は-10^{18}なので、dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i],-10^{18})
つまり
dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i])である。*18
ここで、数列A,B,Cを用意し、それぞれ
A_{j-w[i]}=dp[i][j],B_{j-w[i]}=dp[i][j-w[i]]+v[i],C_{j-w[i]}=dp[i+1][j]とする。
dp[i]は広義単調増加より、数列A,Bともに広義単調増加である。
ここで、dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i])より、C_k=max(A_k,B_k)であるから、補題より、数列Cは広義単調増加である。
よって、dp[i+1][j](w[i]\leqq j\leqq W)の範囲において広義単調増加である。

dp[i+1][w[i]-1]=dp[i][w[i]-1]
dp[i+1][w[i]]=max(dp[i][w[i]],dp[i][0]+v[i])であり、
dp[i]は広義単調増加からdp[i][w[i]-1]\leqq dp[i][w[i]]なので、
dp[i+1][w[i]-1]\leqq dp[i+1][w[i]]である。

これらから、
dp[i+1][j](0\leqq j\lt w[i])dp[i+1][j](w[i]\leqq j\leqq W)
それぞれの範囲で広義単調増加であり、かつ
dp[i+1][w[i]-1]\leqq dp[i+1][w[i]]
であるため、dp[i+1]は広義単調増加である。

(1),(2)より、数学的帰納法から、dp[i](0\leqq i\leqq N)は広義単調増加である。

dp[i+1][k]=max(dp[i+1][k],dp[i][j])(j+1\leqq k\leqq W)の遷移について考える。
dp[i][k]からdp[i+1][k]への遷移が別に存在し、dp[i]は広義単調増加より、dp[i][j]\leqq dp[i][k]なので、dp[i][j]からdp[i+1][k]へ遷移をする必要がない。

dp[i+1][k]=max(dp[i+1][k],dp[i][j]+v[i])(j+w[i]+1\leqq k\leqq W)の遷移について考える。
dp[i][k-w[i]]からdp[i+1][k]への遷移が別に存在し、dp[i]は広義単調増加より、dp[i][j]+v[i]\leqq dp[i][k-w[i]]+v[i]なので、dp[i][j]からdp[i+1][k]へ遷移をする必要がない。

以上から、
dp[i+1][k]=max(dp[i+1][k],dp[i][j])(j+1\leqq k\leqq W)
dp[i+1][k]=max(dp[i+1][k],dp[i][j]+v[i])(j+w[i]+1\leqq k\leqq W)
どちらの遷移もする必要がない。Q.E.D.
ところでこれこんなに証明長くなる???もっといい証明が思いついたら教えてください…

ということで(?)、すべてのdp[i][j]が定義を満たしていることがわかりました。
さて、雑証明を読んでくれた方ならすぐわかると思うのですが、このDPは-INFで初期化しなくても0以下の値で初期化すればOKです。(maxを取るところで遷移に影響しなければよいので)
dp[0][j]は0で初期化しないといけないので、配列をすべて0で初期化するのが一番楽です。
(よく考えると、dp[i][j]=「品物iよりも前の品物の選び方で重さの総和がj以下のものの価値の総和の最大値」で、どの品物までの選び方でも重さの総和が0であるような選び方が必ず存在する*19のですべて初期値が0でも大丈夫ですね うっかりしていました)

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    vector<vector<long long>> dp(N+1,vector<long long>(W+1,0));
    //dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
    //すべて0で初期化しておく
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            dp[i+1][j]=max(dp[i+1][j],dp[i][j]);  //品物iを取らない場合
            if(j+w[i]<=W){
                dp[i+1][j+w[i]]=max(dp[i+1][j+w[i]],dp[i][j]+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    cout << dp[N][W] << endl;
    //答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値
}

実装(Python)

N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
dp=[[0]*(W+1) for i in range(N+1)]
#dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
#すべて0で初期化しておく
for i in range(N):
    for j in range(W+1):
        dp[i+1][j]=max(dp[i+1][j],dp[i][j])  #品物iを取らない場合
        if j+w[i]<=W:
            dp[i+1][j+w[i]]=max(dp[i+1][j+w[i]],dp[i][j]+v[i])
            #品物iを取る場合(重さの総和がW以下)
print(dp[N][W])  #答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値


これはそれなりに短く書けるO(NW)の解法です。

4.結局

(ナップザックDPだけ理解できればOK!という人はここで読むのをやめて大丈夫です)

結局、(ナップザックDPに限らず)DPで大事なのは初期化と遷移です。(当たり前)
こうやって状態をまとめると、この状態の最大値を見つけるにはこの状態の最大値とこの状態の最大値からの遷移だけすればよい!みたいな考え方ができるようになったら、たぶんどんどんDPの問題が解けるようになると思います。
逆に(?)、N次元DPをN次元配列だと意識する必要はそんなにありません。配列の次元は添字を管理するためだけのものであると考えた方が簡単になる場合が多いと思います。
もちろん、いちいち多次元配列を使わずに、C++のmapやPythonのdefaultdictなどで添字を管理することもできなくはないです。
例えば、3-4の一番最後の解法は、

実装(C++)

#include <bits/stdc++.h>
using namespace std;
int main(){
    int N,W;
    cin >> N >> W;
    vector<int> w(N),v(N);
    for(int i=0;i<N;i++){
        cin >> w[i] >> v[i];
    }
    map<pair<int,int>,long long> dp;
    //dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
    for(int i=0;i<N;i++){
        for(int j=0;j<=W;j++){
            dp[{i+1,j}]=max(dp[{i+1,j}],dp[{i,j}]);  //品物iを取らない場合
            if(j+w[i]<=W){
                dp[{i+1,j+w[i]}]=max(dp[{i+1,j+w[i]}],dp[{i,j}]+v[i]);
                //品物iを取る場合(重さの総和がW以下)
            }
        }
    }
    cout << dp[{N,W}] << endl;
    //答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値
}

実装(Python)

from collections import defaultdict
N,W=map(int,input().split())
w=[0]*N
v=[0]*N
for i in range(N):
    w[i],v[i]=map(int,input().split())
dp=defaultdict(int)
#dp[i][j]=品物[0,i)の選び方で重さの総和がj以下のものの価値の総和の最大値とする
for i in range(N):
    for j in range(W+1):
        dp[(i+1,j)]=max(dp[(i+1,j)],dp[(i,j)])  #品物iを取らない場合
        if j+w[i]<=W:
            dp[(i+1,j+w[i])]=max(dp[(i+1,j+w[i])],dp[(i,j)]+v[i])
            #品物iを取る場合(重さの総和がW以下)
print(dp[(N,W)])  #答えは品物[0,N)の選び方で重さの総和がW以下のものの価値の総和の最大値


と書くこともできます。(これはTLEします)
もちろん人々が多次元配列を使うのは、C++ならlogが落ちる、Pythonならtupleを使わなくて済み最悪計算量で抑えられる、かつ書きやすい(こっちは人によるかも?)からです。
しかし、N次元DPをN次元配列として意識しすぎて、表を書いて矢印を伸ばしてみたいなことをするよりは、遷移のみを考える、つまりどの添字からどの添字へどのように遷移するかだけを考えるほうが簡単な場合もあると思います。

5.練習問題

正直この文章をすべて理解出来たらABCのD問題くらいまでのDPは解説を理解できると思うので適当に解いてください(え?????)
最初はこう書いてたんですけどさすがに無責任すぎるのでもう少し真面目に書きます!

D - Poisonous Full-Course
選出理由:この前自分が解いたばかりだったため状態を添字にするDPとしてよい例だったため
状態を添字にするDPです。状態は0と1で持つのが楽です。いい感じにdp[i][j]を自分で定義して解いてみましょう。
よくわからなかったら割と早めに解説を見ても大丈夫です。

D - Shift vs. CapsLock
選出理由:状態を添字にするDPとしてよい例だったため2、DPには色々と持たせることができるよい例だったため
DPが持つのは最小値と最大値だけじゃないということです。
この問題はグラフを使って解くこともできます。

E - Knapsack 2
選出理由:EDPC-Dの次の問題だったため
計算量に気を付けましょう!!!
dpをどう定義して何を持たせれば間に合うかを考える問題です。
よくわからなかったら割と早めに解説を見ても(検索しても)大丈夫です。
これナップザックDPを理解した直後にすんなり解ける人、いるのか???

お疲れさまでした!後は頑張って色んな問題を解きましょう!

6.雑談

ナップザックDPのわかりにくいところって計算量がナップザックの容量で抑えられるところだと思うんですよね
最後にmax(dp[N])を出力するタイプとdp[N][W]を出力するタイプがあるのもややこしそう
そもそも配るDPと貰うDPの2パターンがある時点でDPって難しいと思います

ところで最初PythonのコードをPythonで出していてなんでTLE!?!?となっていました
4.1:動的計画法の復習(ナップサック問題) - HackMDを読んでPyPyの存在を思い出しました Pythonを全然使っていないのがバレてしまう…

*1:要素をデフォルト構築するらしいです 参考:map::operator[] - cpprefjp C++日本語リファレンス

*2:default_factoryというのを指定しているらしいです

*3:int()が呼ばれてカウント0を生成するらしいです 参考:collections --- コンテナデータ型 — Python 3.12.1 ドキュメント

*4:こういうの、最初に言われたとき本当か?となりがち 本当に納得できない人は「二項係数の和」などで検索すると幸せになれるかも?

*5:品物を0から数えて0,1,2,…N-1とするということです

*6:全探索に使っている変数にdpと名付ける勇気がありませんでした

*7:Wikipediaにも『「うつりかわり」のこと』と書いてあったので多分合ってると思います

*8:知ってたら、すみません…

*9:[a,b)a\leqq x \lt bである(一般的には実数)xの集合を表します ここでは[0,i)0\leqq x \lt iである整数xを表しています 参考:区間 (数学) - Wikipedia開区間,閉区間の意味と関連する話題 | 高校数学の美しい物語

*10:mapは要素数Nのとき値へのアクセス、値の追加どちらも\log Nかかります

*11:最悪計算量ではなく平均計算量ということです 気になる人は適宜検索してください、すみません…

*12:dictは要素数Nのとき値へのアクセス、値の追加どちらも平均O(1)、最悪O(N)かかりますが、競プロの問題では基本O(N)かかることはないと考えてよいらしいです 参考:Pythonのハッシュ衝突攻撃の考察 #Python - Qiita 追記(8/27):そんなこともないらしいです 参考:Pythonのハッシュ衝突攻撃の考察2: 辞書のキー検索を故意に衝突させられました #Python - Qiita

*13:Keyの上限が小さいとわかっている場面で辞書型ではなくただの配列を使って高速化するのはたまに使うテクニックです

*14:例えばi=0のときにsum_wとしてあり得る値は0だけですが、工夫をしないと(重さの総和,価値の総和)=(1,配列の初期値)や(W,配列の初期値)などの存在しないはずのペアが存在してしまいます

*15:すべての品物を選ばないという選び方が必ず存在します

*16:本当は\Omega(NW^2)などと書くべきですが、ここでそういう表記をするのはちょっとな~という気分になってしまいました 気になる人は適宜検索してください、すみません…

*17:0\leqq x\leqq Wなので、0\leqq W-x\leqq Wです 自明かも

*18:貰うDPが出てきて自分でもびっくり

*19:品物iまでの品物をすべて選ばないという選び方のことです