AOJ 2614 Almost Same Substring

解法

ローリングハッシュでやります.

同じ長さの文字列 S, T に対して,「左からみて何文字一致しているか」と「右からみて何文字一致しているか」をそれぞれ二分探索で求めます.
もし S と T が一文字だけ異なるなら,左からの一致文字数と右からの一致文字数の和が,長さから1だけ小さいものになっているはずです.

よって,問題で与えられた S, T' について,S[a..a+|T'|-1] と T' に対し,先程やったことをすれば求まります.

ソースコード

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

// 1-indexed
// s[1...n]
class rolling_hash {
public:
    rolling_hash(std::string const& s)
        : n(s.size()),
          mod({999999937LL, 1000000007LL})
    {
        for(int i = 0; i < 2; ++i) {
            hs[i].resize(n + 1);
            p[i].resize(n + 1);
            hs[i][0] = 0;
            p[i][0] = 1;
            for(int j = 0; j < n; ++j) {
                hs[i][j + 1] = (hs[i][j] * base + s[j]) % mod[i];
                p[i][j + 1] = p[i][j] * base % mod[i];
            }
        }
    }

    // s[i...]
    long long query(int idx, int i) const {
        return hs[i][idx];
    }
    // s[l + 1...r]
    long long query(int l, int r, int i) const {
        return ((hs[i][r] - hs[i][l] * p[i][r - l]) % mod[i] + mod[i]) % mod[i];
    }

private:
    int n;
    std::vector<long long> hs[2];
    std::vector<long long> p[2];

    const std::array<long long, 2> mod;
    static const long long base = 29LL;
};

int main() {
    string S, T;
    cin >> S >> T;
    rolling_hash hs1(S), hs2(T);
    int res = 0;
    for(int i = 1; i + T.size() - 1 <= S.size(); ++i) {
        int lb = i - 1, ub = i + T.size();
        while(ub - lb > 1) {
            int m = (ub + lb) / 2;
            if(hs1.query(i - 1, m, 1) == hs2.query(0, m - i + 1, 1)) {
                lb = m;
            } else {
                ub = m;
            }
        }
        int l = lb;
        lb = i - 1, ub = i + T.size();
        while(ub - lb > 1) {
            int m = (ub + lb) / 2;
            if(hs1.query(m - 1, i + T.size() - 1, 1) == hs2.query(m - i, T.size(), 1)) {
                ub = m;
            } else {
                lb = m;
            }
        }
        int r = ub;

        res += l + 2 == r;
    }
    cout << res << endl;
}

感想

ローリングハッシュ使う時いっつもインデックスで混乱する.

AOJ 2605 Social Monsters

解法

制約をよく読むと,(使えない辺を含める)与えられたグラフにおける各連結成分は,1つのパスまたは閉路になっていることがわかります.
各連結成分において計算して,最後にまとめあげることにします.

1つのパスについては,以下のDPで端から順番に計算します.
dp[i][j][k] := i 番目の頂点までみたとき,それまでに j 個の頂点を用いていて,直前の頂点を使ったかのフラグが k であるときの最大値
隣接してる2つが使えないなら,k == 0 となる場合しか今見ている頂点は使えない,みたいな遷移になります.

閉路については,追加で情報が必要です.閉路の始点は適当に決めて良いです.
dp[i][j][k][l] := i 番目の頂点まで見た時,それまでに j 個の頂点を用いていて,直前の頂点を使ったかのフラグが k であり,最初の頂点を使ったかのフラグが l であるときの最大値

最後に各連結成分でもとめた dp の値を合わせて全体の結果を得ます.

ソースコード

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

constexpr int INF = 1e9;

struct edge {
    int to, cost;
};

using edges = vector<edge>;
using graph = vector<edges>;

int N, M, K;
int cost[2001][2001];

// true -> circuit
bool dfs(graph const& g, int v, int p, vector<bool>& visited, vector<int>& res) {
    res.push_back(v);
    visited[v] = true;
    bool circuit = false;
    for(auto& e : g[v]) {
        if(e.to == p) {
            continue;
        }
        if(visited[e.to]) {
            circuit = true;
        } else {
            circuit |= dfs(g, e.to, v, visited, res);
        }
    }
    return circuit;
}

vector<int> calc_line(vector<int> const& line) {
    vector<vector<int>> dp(K + 1, vector<int>(2, -INF));
    dp[0][0] = dp[1][1] = 0;
    for(int i = 1; i < (int)line.size(); ++i) {
        int pre = line[i - 1];
        int v = line[i];
        vector<vector<int>> nxt(K + 1, vector<int>(2, -INF));
        for(int k = 0; k <= K; ++k) {
            if(dp[k][0] != -INF) {
                if(k + 1 <= K) {
                    nxt[k + 1][1] = max(nxt[k + 1][1], dp[k][0]);
                }
                nxt[k][0] = max(nxt[k][0], dp[k][0]);
            }
            if(dp[k][1] != -INF) {
                if(cost[pre][v] != 0 && k + 1 <= K) {
                    nxt[k + 1][1] = max(nxt[k + 1][1], dp[k][1] + cost[pre][v]);
                }
                nxt[k][0] = max(nxt[k][0], dp[k][1]);
            }
        }
        dp.swap(nxt);
    }

    vector<int> res(K + 1);
    for(int i = 0; i < K + 1; ++i) {
        res[i] = max(dp[i][0], dp[i][1]);
    }
    return res;
}

vector<int> calc_cycle(vector<int> const& cycle) {
    // (use, pre, first)
    vector<vector<vector<int>>> dp(K + 1, vector<vector<int>>(2, vector<int>(2, -INF)));
    dp[0][0][0] = dp[1][1][1] = 0;
    int const first = cycle[0];
    for(int i = 1; i < (int)cycle.size(); ++i) {
        int pre = cycle[i - 1];
        int v = cycle[i];
        vector<vector<vector<int>>> nxt(K + 1, vector<vector<int>>(2, vector<int>(2, -INF)));
        for(int k = 0; k <= K; ++k) {
            for(int fuse = 0; fuse <= 1; ++fuse) {
                if(i == cycle.size() - 1 && fuse == 1) {
                    if(dp[k][0][fuse] != -INF) {
                        if(k + 1 <= K && cost[first][v] != 0) {
                            nxt[k + 1][1][fuse] = max(nxt[k + 1][1][fuse], dp[k][0][fuse] + cost[first][v]);
                        }
                        nxt[k][0][fuse] = max(nxt[k][0][fuse], dp[k][0][fuse]);
                    }
                    if(dp[k][1][fuse] != -INF) {
                        if(cost[first][v] != 0 && cost[pre][v] != 0 && k + 1 <= K) {
                            nxt[k + 1][1][fuse] = max(nxt[k + 1][1][fuse], dp[k][1][fuse] + cost[pre][v] + cost[first][v]);
                        }
                        nxt[k][0][fuse] = max(nxt[k][0][fuse], dp[k][1][fuse]);
                    }
                } else {
                    if(dp[k][0][fuse] != -INF) {
                        if(k + 1 <= K) {
                            nxt[k + 1][1][fuse] = max(nxt[k + 1][1][fuse], dp[k][0][fuse]);
                        }
                        nxt[k][0][fuse] = max(nxt[k][0][fuse], dp[k][0][fuse]);
                    }
                    if(dp[k][1][fuse] != -INF) {
                        if(cost[pre][v] != 0 && k + 1 <= K) {
                            nxt[k + 1][1][fuse] = max(nxt[k + 1][1][fuse], dp[k][1][fuse] + cost[pre][v]);
                        }
                        nxt[k][0][fuse] = max(nxt[k][0][fuse], dp[k][1][fuse]);
                    }
                }
            }
        }
        dp.swap(nxt);
    }

    vector<int> res(K + 1);
    for(int i = 0; i < K + 1; ++i) {
        res[i] = max({dp[i][0][0], dp[i][0][1], dp[i][1][0], dp[i][1][1]});
    }
    return res;
}

int main() {
    cin >> N >> M >> K;
    graph g(N);
    for(int i = 0; i < M; ++i) {
        int a, b, c;
        cin >> a >> b >> c;
        a--; b--;
        g[a].push_back(edge{b, c});
        g[b].push_back(edge{a, c});
        cost[a][b] = cost[b][a] = c;
    }

    vector<vector<int>> line, cycle;
    vector<bool> visited(N);
    vector<int> sz;
    for(int i = 0; i < N; ++i) {
        if(!visited[i]) {
            vector<int> c;
            bool circuit = dfs(g, i, -1, visited, c);
            if(!circuit) {
                for(auto v : c) {
                    visited[v] = false;
                }
                for(auto v : c) {
                    if(g[v].size() == 1) {
                        c.clear();
                        dfs(g, v, -1, visited, c);
                        break;
                    }
                }
                sz.push_back(c.size());
                line.push_back(move(c));
            } else {
                cycle.push_back(move(c));
            }
        }
    }
    for(auto& c : cycle) {
        sz.push_back(c.size());
    }

    vector<vector<int>> dp2;
    for(auto& l : line) {
        dp2.push_back(calc_line(l));
    }
    for(auto& c : cycle) {
        dp2.push_back(calc_cycle(c));
    }

    vector<int> dp(K + 1, -INF);
    dp[0] = 0;
    int total_sz = 0;
    for(int i = 0; i < (int)dp2.size(); ++i) {
        vector<int> nxt(K + 1, -INF);
        for(int k1 = 0; k1 <= total_sz; ++k1) {
            if(dp[k1] == -INF) {
                continue;
            }
            for(int k2 = 0; k2 <= sz[i] && k1 + k2 <= K; ++k2) {
                if(dp2[i][k2] != -INF) {
                    nxt[k1 + k2] = max(nxt[k1 + k2], dp[k1] + dp2[i][k2]);
                }
            }
        }
        total_sz += sz[i];
        dp.swap(nxt);
    }
    if(dp[K] == -INF) {
        cout << "Impossible" << endl;
    } else {
        cout << dp[K] << endl;
    }
}

感想

実装が重い.うまくやれば実装量は減りそうだが,半分ぐらいは同じことをしてるコードなので,コード長の割には楽だったかもしれない.

AOJ 2449 Connect

解法

これは典型問題です.
$i$ 行目の $j$ 番目に文字を置くかどうか考えた時に関係してくるのは,$i$ 行目の $1$ から $j - 1$ 列目と,$i - 1$ 行目の $j$ から $C$ 列目だけです.
なので結局,一行分の状態をビットで持つ bitDP にできます.

$dp[i][j][S] := i$ 行目の $j$ 列目まで見た時,文字が配置されている状況が $S$ であるときの最大値
$S$ の下位 $j$ ビットが今の行の状態,上位 $C - j$ ビットが直前の行の状態を表しています.

$i$ 行目 $j$ 列目の位置に文字を配置する場合,直前に同じ文字が置かれているかと,前の行の同じ列に同じ文字が置かれているかを見ます.
今配置しようとしている文字のインデックスは $bitcount(S \& (1 << j) - 1)$ で得られます.
上の行で配置されている文字のインデックスは,後ろから何番目であるかを考えれば,$|s_i| - bitcount(S >> j)$ で得られます.

使わない選択をする場合は,あとに配置できる余地が十分であるかを確認する必要があります.
その他,$S$ の状態でありえないものは適宜無視すればよいです.

ソースコード

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

int main() {
    int R, C;
    cin >> R >> C;
    vector<string> s(R);
    for(int i = 0; i < R; ++i) {
        cin >> s[i];
    }
    vector<int> bitcount(1 << C);
    for(int i = 0; i < 1 << C; ++i) {
        bitcount[i] = __builtin_popcount(i);
    }

    vector<int> dp(1 << C, -1);
    dp[0] = 0;
    for(int i = 0; i < R; ++i) {
        for(int j = 0; j < C; ++j) {
            vector<int> nxt(1 << C, -1);
            for(int S = 0; S < (1 << C); ++S) {
                if(dp[S] == -1) {
                    continue;
                }
                int idx = bitcount[S & (1 << j) - 1];
                if(idx < s[i].size()) {
                    int t = dp[S];
                    if(idx > 0 && (S & 1 << (j - 1)) && s[i][idx - 1] == s[i][idx]) {
                        t++;
                    }
                    if(i > 0 && (S & 1 << j) && s[i][idx] == s[i - 1][s[i - 1].size() - bitcount[S >> j]]) {
                        t++;
                    }
                    nxt[S | 1 << j] = max(nxt[S | 1 << j], t);
                }
                if(C - j > s[i].size() - idx) {
                    nxt[S & ~(1 << j)] = max(nxt[S & ~(1 << j)], dp[S]);
                }
            }
            dp.swap(nxt);
        }
    }
    cout << 2 * *max_element(begin(dp), end(dp)) << endl;
}

AOJ 2376 Disconnected Game

解法

石取りゲームだからいい感じにわかるんでは?と思ったけどよくわかんなかったのでメモ化再帰で解きます.

勝ちが決まるタイミングはは,連結成分の数が2であり,かつ自分の手番で辺を追加すると,追加できる辺の数が 0 になるときですね.(それはそう)
それまでは,異なる連結成分を連結させるか,連結成分内の頂点同士に辺を張るかのどちらかです.

残りの辺の数は愚直にもたずに偶奇で判定できますね.異なる連結成分同士を結ぶ辺以外で使える辺の数を $unused$ とします.

異なる連結成分同士を結んだ時,$unused$ も変化しますが,偶奇だけわかればいいので,連結成分のサイズの偶奇の情報を持つだけでいいです.
今,サイズが奇数,偶数の連結成分の数を $(odd, even)$ とします.

$(odd, even, unused)$ でメモ化再帰します.
自分の手番でできるパターンは以下の4通りあります.

  • 大きさが偶数の連結成分同士を結ぶ.結果として,偶数の連結成分が1つ減り,新たに使える辺が奇数本増える.
  • 大きさが奇数の連結成分同士を結ぶ.結果として,奇数の連結成分が2つ減り,偶数の連結成分が1つ増える.また,使える辺の数が偶数本増える.
  • 大きさが偶数と奇数の連結成分を結ぶ.結果として,偶数の連結成分が1つ減る.また,使える辺の数が奇数本増える.
  • 連結成分同士を結ばないように辺を張る.

4つ目の場合はメモ化再帰でループが発生してしまいますが,実は $unused$ を偶数から奇数に移す場合は無視できます.
なぜなら,こういう操作をされた場合,次の手番の人が $unused$ を奇数から偶数に移せば,先ほどと同じ状態になるため,いずれは連結成分同士を結ぶ状況に陥るからです.(つまり偶数から奇数に移す操作が最善手であることはない.そういう状況はすでに負け確定.)
これでループが解消できたので,安心してかけます.

ソースコード

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

int dfs(int v, vector<string> const& g, vector<bool>& visited) {
    visited[v] = true;
    int res = 1;
    for(int i = 0; i < (int)g.size(); ++i) {
        if(g[v][i] == 'Y' && !visited[i]) {
            res += dfs(i, g, visited);
        }
    }
    return res;
}

int rec(int odd, int even, int unused, vector<vector<vector<int>>>& dp) {
    int& res = dp[odd][even][unused];
    if(res != -1) {
        return res;
    }

    if(odd + even == 2) {
        return res = unused;
    }
    if(unused == 1 && !rec(odd, even, !unused, dp)) {
        return res = 1;
    }
    if(odd > 0 && even > 0 && !rec(odd, even - 1, !unused, dp)) {
        return res = 1;
    }
    if(odd >= 2 && !rec(odd - 2, even + 1, unused, dp)) {
        return res = 1;
    }
    if(even >= 2 && !rec(odd, even - 1, !unused, dp)) {
        return res = 1;
    }
    return res = 0;
}

int main() {
    int N;
    cin >> N;
    vector<string> g(N);
    for(int i = 0; i < N; ++i) {
        cin >> g[i];
    }

    vector<bool> visited(N);
    int odd = 0, even = 0;
    int unused = 0;
    for(int i = 0; i < N; ++i) {
        if(!visited[i]) {
            int C = dfs(i, g, visited);
            unused += C * (C - 1) / 2;
            if(C & 1) {
                ++odd;
            } else {
                ++even;
            }
        }
        for(int j = i + 1; j < N; ++j) {
            unused -= (g[i][j] == 'Y');
        }
    }
    unused %= 2;

    vector<vector<vector<int>>> dp(N + 1, vector<vector<int>>(N + 1, vector<int>(2, -1)));
    if(rec(odd, even, unused, dp)) {
        cout << "Taro" << endl;
    } else {
        cout << "Hanako" << endl;
    }
}

感想

4つ目のパターンを書き忘れた状態で間違えて提出してしまったんですが,なぜか通ってしまい,ジャッジのケースが弱いのか,実は完全に無視して良いのかよくわからなくなった….
もしあるなら O(1) 解をもっと考えればそのうちわかるかも(考えませんけどね).

AOJ 2313 Box Witch

解法

ちゃんとフローがわかってるなら解ける問題.
まず辺の追加クエリは簡単で,単純に辺を増やすだけ.
問題は辺の削除クエリです.与えられた辺を $\{a, b\}$ とします.
今流れてるフローで与えられた辺が使われていないなら,単純に削除します(これは辺のキャパシティ $cap[u][v]$ を持っておいて,$cap[a][b] \neq 1$ であるかで判定できます.どっちが 0 かで流れている向きもわかる.)
使われている場合,残余グラフ上で $a \rightarrow b$ のパスが残っているなら,$a \rightarrow b$ のフローを1ながして $\{a, b\}$ を消せば,整合性を損なうことなく流量を維持できます.
もし $a \rightarrow b$ のパスがない場合,一旦 $n \rightarrow 1$ へのフローを1流し直して,そのあと $a \rightarrow b$ のフローを流すと,自然にもともとの $a \rightarrow b$ のフローを打ち消すことが出来ます.
あとは辺を消してフローを流し直すだけで終わります.

ソースコード

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

using graph = vector<vector<int>>;

int dfs(graph& g, vector<bool>& visited, vector<vector<int>>& cap, int v, int t) {
    if(v == t) {
        return 1;
    }
    visited[v] = true;
    for(auto to : g[v]) {
        if(visited[to] || cap[v][to] == 0) {
            continue;
        }
        if(dfs(g, visited, cap, to, t) == 1) {
            cap[to][v]++;
            cap[v][to]--;
            return 1;
        }
    }
    return 0;
}

int max_flow(graph& g, vector<vector<int>>& cap, int s, int t, int cnt = 100000) {
    vector<bool> visited(g.size());
    int f = 0;
    while(cnt--) {
        fill(visited.begin(), visited.end(), false);
        if(dfs(g, visited, cap, s, t) == 1) {
            f++;
        } else {
            break;
        }
    }
    return f;
}

int main() {
    int N, E, Q;
    cin >> N >> E >> Q;
    graph g(N);
    vector<vector<int>> cap(N, vector<int>(N));
    auto add_edge = [&](int from, int to) {
        g[from].push_back(to);
        g[to].push_back(from);
        cap[from][to] = cap[to][from] = 1;
    };
    for(int i = 0; i < E; ++i) {
        int f, t;
        cin >> f >> t;
        f--; t--;
        add_edge(f, t);
    }

    int res = max_flow(g, cap, 0, N - 1);
    while(Q--) {
        int m, a, b;
        cin >> m >> a >> b;
        a--; b--;
        if(m == 1) {
            add_edge(a, b);
        } else {
            // used b -> a
            if(cap[a][b] == 2) {
                // doesn't exist circuit b -> a on residual graph
                if(max_flow(g, cap, b, a, 1) == 0) {
                    max_flow(g, cap, N - 1, 0, 1);
                    max_flow(g, cap, b, a, 1);
                    res--;
                }
            } else if(cap[b][a] == 2) {
                if(max_flow(g, cap, a, b, 1) == 0) {
                    max_flow(g, cap, N - 1, 0, 1);
                    max_flow(g, cap, a, b, 1);
                    res--;
                }
            }

            cap[a][b] = cap[b][a] = 0;
            g[a].erase(find(g[a].begin(), g[a].end(), b));
            g[b].erase(find(g[b].begin(), g[b].end(), a));
        }

        res += max_flow(g, cap, 0, N - 1);
        cout << res << endl;
    }
}

感想

フローのいい練習になりそうな問題.

AOJ 2344 Multi Ending Story

解法

木DPで解きます.
まず部分木に注目しましょう.
冷静に考えると,この部分木のすべての葉を回収するのには,以下の3通りだけしかありません.この部分木の根を $v$,左右の子をそれぞれ $l, r$ とします.

  • $v$ で quick save を使わず,それぞれの子の部分木でいい感じに quick save を用いて最適に回収する
  • $v$ で quick save した後,まず $l$ の部分木の葉をすべて回収(この時 quick save は用いない)して,その後 $r$ の部分木で quick save を用いつつ最適に回収する
  • 2つ目のパターンで左右を入れ替えたパターン

そこで以下のように定めます.
$dp[v] := v$ が根の部分木にあるすべての葉を quick save をいい感じに用いて最適に回収したときのコスト
$dp_2[v] := v$が根の部分木にあるすべての葉を, $v$ で quick save をしたとき(子の部分木では一切 quick save しない)の回収のコスト.ただし真の根から $v$ までのコストは除くものとする.
$cnt[v] := v$ の部分木に含まれる葉の個数

1つ目のパターンは,$v$ で quick save しないので真の根から $v$ までのコスト $depth[v]$ が余計にかかります.なので,このときのコストは
$dp[l] + dp[r] + depth[v] + 2$
となります.

2つ目(3つ目も同様)のパターンでは,$l$ の部分木の回収に $dp_2[l]$ と,
$v$ から $l$ に $cnt[l]$ 回移動することになるのでその分だけのコストがまずかかります.その後は,$r$ にうつって最適に回収するので,そのコストは $dp[r] + 1$ です.よって総コストは
$dp_2[l] + cnt[l] + dp[r] + 1$
となります.

あとはこれらのうちで最小のものが $dp[v]$ の値となります.

ソースコード

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

using ll = long long;

// スタック拡張します

int N;
ll dp[500001], dp2[500001];
int leaf_cnt[500001];
int g[500001][2];

ll dfs(int v, int depth) {
    for(int i = 0; i < 2; ++i) {
        int to = g[v][i];
        if(to == N) {
            leaf_cnt[v]++;
        } else {
            dfs(to, depth + 1);
            leaf_cnt[v] += leaf_cnt[to];
            dp2[v] += dp2[to];
        }
    }
    dp2[v] += leaf_cnt[v];

    ll& res = dp[v];
    int l = g[v][0], r = g[v][1];
    dp[v] = min({dp2[l] + (leaf_cnt[l] == 0 ? 1 : leaf_cnt[l]) + 1 + dp[r],
                 dp[l] + dp2[r] + (leaf_cnt[r] == 0 ? 1 : leaf_cnt[r]) + 1,
                 dp[l] + depth + dp[r] + 2});
    return dp[v];
}

int main() {
    cin >> N;
    for(int i = 0; i < N - 1; ++i) {
        int a, b;
        cin >> a >> b;
        a -= (a != N);
        b -= (b != N);
        g[i][0] = a;
        g[i][1] = b;
    }
    cout << dfs(0, 0) << endl;    
}

感想

メモリの制約が厳しい上に,普通にスタックオーバーフローするので†スタック拡張†します.
DP部分は普通に難しかった…….

AOJ 2598 Website Tour

解法

移動に時間がかからないので,各強連結成分においては,好きな広告を(制限以内なら)好きな回数だけ見ることが出来ます.これは典型的な個数制限付きナップザック問題です.

あとは強連結成分分解をしたグラフで,トポロジカル順序でDP(DAGの上のDP)をします.
各強連結成分では,先に述べた個数制限付きナップザック問題を解きますが,自己辺が無いようなそれ自身だけからなる強連結成分は,自分の広告を何回も見ることはどうやっても出来ないので,個数制限を 1 と改めておきます.
あとは普通のナップザック問題で,
$dp[i][j] := $ 頂点 $i$ まで見たときに,時間の総和が $j$ 以下であるような広告の見方をしたときの最大値
として,各頂点で答えが求まったら,子に対して $dp[child][j] = dp[i][j]$ とかするだけでよいです.

ソースコード

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

using pii = pair<int, int>;

struct edge {
    int from, to;
};

using edges = std::vector<edge>;
using graph = std::vector<edges>;

void add_edge(graph& g, int from, int to) {
    g[from].push_back((edge){from, to});
}

int scc(graph& G, std::vector<int>& cmp) {
    int V = G.size();
    std::vector<std::vector<int>> g(V), rg(V);
    std::vector<bool> used(V, false);
    std::vector<int> vs;
    cmp.resize(V);
    for(int i=0; i<V; ++i) {
        for(auto e : G[i]) {
            g[i].push_back(e.to);
            rg[e.to].push_back(i);
        }
    }
    std::function<void(int)> dfs = [&g, &vs, &used, &dfs](int v) {
        used[v] = true;
        for(auto i : g[v]) {
            if(!used[i]) {
                dfs(i);
            }
        }
        vs.push_back(v);
    };
    std::function<void(int, int)> rdfs = [&rg, &cmp, &used, &rdfs](int v, int k) {
        used[v] = true;
        cmp[v] = k;
        for(int i : rg[v]) {
            if(!used[i]) {
                rdfs(i, k);
            }
        }
    };
    for(int v=0; v<V; ++v) {
        if(!used[v]) {
            dfs(v);
        }
    }
    std::fill(used.begin(), used.end(), false);
    int k = 0;
    for(int i=vs.size()-1; i>=0; --i) {
        if(!used[vs[i]]) {
            rdfs(vs[i], k++);
        }
    }
    return k;
}

std::vector<std::vector<int>> build_graph(graph const& g, std::vector<int> const& cmp, int K) {
    int V = g.size();
    std::vector<std::set<int>> s(K);
    std::vector<std::vector<int>> res(K);
    for(int i=0; i<V; ++i) {
        for(auto e : g[i]) {
            s[cmp[i]].insert(cmp[e.to]);
        }
    }
    for(int i=0; i<K; ++i) {
        for(auto j : s[i]) {
            if(i != j) {
                res[i].push_back(j);
            }
        }
    }
    return res;
}


int main() {
    int N, M, T;
    while(cin >> N >> M >> T, N) {
        graph g(N);
        vector<int> p(N), t(N), k(N);
        vector<bool> self(N);
        for(int i = 0; i < N; ++i) {
            cin >> p[i] >> t[i] >> k[i];
        }
        for(int i = 0; i < M; ++i) {
            int a, b;
            cin >> a >> b;
            a--; b--;
            add_edge(g, a, b);
            if(a == b) {
                self[a] = true;
            }
        }
        
        vector<int> cmp;
        int const K = scc(g, cmp);
        auto g2 = build_graph(g, cmp, K);
        vector<vector<int>> C(K);
        for(int i = 0; i < N; ++i) {
            C[cmp[i]].push_back(i);
        }

        vector<vector<int>> dp(K, vector<int>(T + 1));
        for(int i = 0; i < K; ++i) {
            if(C[i].size() == 1 && !self[C[i][0]]) {
                k[C[i][0]] = 1;
            }
            for(auto v : C[i]) {
                for(int j = 0; j < t[v]; ++j) {
                    deque<pii> deq;
                    for(int l = 0; l * t[v] + j <= T; ++l) {
                        int val = dp[i][l * t[v] + j] - l * p[v];
                        while(deq.size() > 0 && deq.back().second <= val) {
                            deq.pop_back();
                        }
                        deq.emplace_back(l, val);
                        dp[i][l * t[v] + j] = deq.front().second + l * p[v];
                        if(deq.front().first == l - k[v]) {
                            deq.pop_front();
                        }
                    }
                }
                for(auto to : g2[i]) {
                    for(int j = 0; j <= T; ++j) {
                        dp[to][j] = max(dp[to][j], dp[i][j]);
                    }
                }
            }
        }

        int res = 0;
        for(int i = 0; i < K; ++i) {
            res = max(res, dp[i][T]);
        }
        cout << res << endl;
    }
}

感想

最初何を血迷ったか,DFSしながら各連結成分ごとに dp2[i][j] を計算し,dp[i][j] = max(dp[ch][j - t], dp2[i][t]) みたいに実装しようとしていて O(nT^2) でだめだなーとかアホな事を考えていた.