問題
https://codeforces.com/contest/1886/problem/E (2400diff)
人のプログラマーがいて、人目の耐久力はである。
個のプロジェクトがあって、個目のプロジェクトの難易度はである。
あなたは、各プロジェクトにプログラマーを割り当てる。以下の条件を全て満たす割当てが存在するか判定し、存在する場合はそのような割当てを一つ求めよ。
- 各プロジェクトに最低一人のプログラマーが割り当てられる
- 各プログラマーは高々一つのプロジェクトに割り当てられる
- 個目のプロジェクトに人のプログラマーが割り当てられたとする。個目のプロジェクトに割り当てられた各プログラマーの耐久力が以上である
制約
解法
プログラマーを昇順に並べ替えたとする。各プロジェクトへのプログラマーの割当てを「前から人目から、前から人目までを割り当てる」として良い。すなわち、数列から個の区間を削除する問題になる。プロジェクトと区間の左端の組に対して、あり得る最左の右端を前計算しておく。これをとする。右端の候補が存在しないときはとする。
を「プロジェクトの集合に対しての前から最低何人いれば割当がうまくいくか」とする。
と遷移できる。これはを後ろから累積min列を前計算しておけば効率的に計算できる。
割当を一つ求める -> dpの復元
実装
#include <bits/stdc++.h> #include "Src/Template/IOSetting.hpp" #include "Src/Number/IntegerDivision.hpp" using namespace zawa; int main() { SetFastIO(); int n, m; std::cin >> n >> m; std::vector<std::pair<int, int>> ai(n); for (int i{} ; i < n ; i++) { std::cin >> ai[i].first; ai[i].second = i; } std::sort(ai.begin(), ai.end()); std::vector<int> b(m); for (auto& x : b) std::cin >> x; const int INF{(int)1e9}; std::vector jump(m, std::vector<int>(n, INF)); for (int i{} ; i < m ; i++) { for (int j{} ; j < n ; j++) { int needK{DivCeil(b[i], ai[j].first)}; jump[i][j] = j + needK; } } std::vector mins(m, std::vector<int>(n, INF)); for (int i{} ; i < m ; i++) { mins[i][n - 1] = jump[i][n - 1]; for (int j{n - 2} ; j >= 0 ; j--) { mins[i][j] = std::min(mins[i][j + 1], jump[i][j]); } } std::vector<std::vector<int>> ans(m); std::vector<int> dp((1 << m), INF); std::vector<int> recov((1 << m), -1); dp[0] = 0; for (int bit{} ; bit < (1 << m) ; bit++) { if (dp[bit] >= n) continue; for (int i{} ; i < m ; i++) { if (bit & (1 << i)) continue; int cur{dp[bit]}; if (mins[i][cur] <= n and dp[bit | (1 << i)] > mins[i][cur]) { dp[bit | (1 << i)] = mins[i][cur]; recov[bit | (1 << i)] = i; } } } if (dp[(1 << m) - 1] == INF) { std::cout << "NO" << '\n'; return 0; } assert(dp[(1 << m) - 1] <= n); int now{(1 << m) - 1}; while (recov[now] != -1) { int use{recov[now]}; assert(now & (1 << use)); int next{now ^ (1 << use)}; assert(dp[next] < dp[now]); int cur{dp[next]}; assert(cur < n); for (int k{1} ; ; k++) { int check{dp[now] - k}; assert(0 <= check and check < n); ans[use].push_back(ai[check].second); int need{DivCeil(b[use], ai[check].first)}; if (check + need <= dp[now]) break; } now = next; } assert(now == 0); std::cout << "YES" << '\n'; for (int i{} ; i < m ; i++) { std::cout << ans[i].size(); for (auto x : ans[i]) std::cout << ' ' << x + 1; std::cout << '\n'; } }
感想
- PC触れないときに時間かけて考察 -> 詰めきれなかったので解説チラ見 -> 「計算量」という文言が見えて全てを理解
- 区間を抜き出す問題と言い換えるまで自力で導出できた。dpがどうしてもなどになって困っていた。解説見てからだとどうして気づけなかったんだろうという気持ちになる
- 実装が下手くそで、6回ペナを出したし愚直も書いた