OUPC2020 E. Xor Mart 解説
人生で初めて作った問題。いかがだったでしょうか。
問題へのリンク
略解
半分全列挙とAtCoderのXXORで出てくる考え方を組み合わせると解けます。
解説
この問題において、「支払う金額を各商品の金額の総和で計算する」と問題の設定を変更した場合、これは典型的なナップサック問題と等しくなります。
上記のような設定の変更を行った問題は、 が充分小さいので半分全列挙+金額に対する二分探索による結果のマージを行うことで解くことができます。
(参考: AtCoder ABC032 D)
しかし、この問題においては「支払う金額を各商品の金額の排他的論理和で計算する」ため単調性が成立しなくなります。 なので、金額に対する二分探索による結果のマージは利用できません。
そこで、他の方法による結果のマージを考えます。
まず、商品の集合を2つの集合 , に分割するものとします。また、 および から選ばれた商品の集合における支払う金額をそれぞれ , とします。
さらに、最終的に支払う金額を とすると、 を満たす必要があります。
このとき、 を2進数表記したもののprefixに関して分類を行うと、
実は高々 種類しかないことがわかります。
より、 のprefix および を固定すると のprefixは一意に定まります。
同じprefixを持つものについてはまとめて計算することが可能(詳細はwriter解を見てください)なので、
のprefix および を全探索することで解を求めることができます。
writerの実装の計算量は となっています。
prefixに関する分類
「 を2進数表記したもののprefixに関する分類」について例を用いて説明します。
例えば、 を10進数表記で 150 とすると、2進数で表すと 10010110 となります。
ここで、0と1のどちらでも良いビットを X と表記することにすると、
のprefixは必ず以下のどれかになります。
- 0XXXXXXX
- 1000XXXX
- 100100XX
- 1001010X
- 10010110
このような分類は、 の値を最上位ビットから順番に決定していくことで得られます。
の値が 未満となることが確定した場合、残りのビットは必ずXとなります。
このような分類の数は、高々 個しかありません。
(このような分類方法は、桁DPなどで使われている考え方です。けんちょんさんの記事が参考になると思います。)
writer解
#include <algorithm> #include <iostream> #include <map> #include <utility> #include <vector> using namespace std; using ll = long long; using P = pair<ll, ll>; constexpr char newl = '\n'; constexpr ll INF = 1e16; // 半分全列挙する関数 vector<P> rekkyo(int L, int R, vector<ll>& A, vector<ll>& B) { int n = R - L; map<ll, ll> memo; for (int i = 0; i < (1 << n); i++) { ll money = 0; ll value = 0; for (int j = 0; j < n; j++) { if (i & (1 << j)) { money ^= A[L + j]; value += B[L + j]; } } // memo[money] = max(memo[money], value); だけだとダメ!!! // memo[money]でやるとデフォルトの値が0になるが、 // valueが負になる場合があるので死ぬ if (memo.find(money) == memo.end()) memo[money] = value; else memo[money] = max(memo[money], value); } vector<P> res; for (auto& p : memo) res.push_back(p); return res; } // 0と1のどちらでも良いビットを切り捨てて、同じprefixのものをまとめていくための関数 vector<P> update(const vector<P>& v, int shift) { // rekkyo() により v はソートされており、 // なおかつ、update()によりソートの順番が崩壊することはないので線形で処理が可能 if (shift == 0) return v; // shiftしないなら v は変化しない vector<P> nex; for (const P& p : v) { ll tar = (p.first >> shift); if (!nex.empty() && nex.back().first == tar) { nex.back().second = max(nex.back().second, p.second); } else { nex.emplace_back(tar, p.second); } } return nex; } ll solve(int N, ll M, vector<ll>& A, vector<ll>& B) { vector<P> v1 = rekkyo(0, N / 2, A, B); vector<P> v2 = rekkyo(N / 2, N, A, B); ll res = 0; while (M >= 0) { // prefixに関する分類をMから順番に降順に見ていく for (P& p : v1) { ll tar = p.first ^ M; auto it = lower_bound(v2.begin(), v2.end(), P(tar, -INF)); if (it == v2.end()) continue; // 不正な値にアクセスしないように P p2 = *it; if (p2.first != tar) continue; ll value = p.second + p2.second; res = max(res, value); } if (M == 0) break; // 次に見るprefixを計算 int shift = 0; while (M > 0 && (M & 1) == 0) { M >>= 1; shift++; } M ^= 1; v1 = update(v1, shift); v2 = update(v2, shift); } return res; } int main() { cin.tie(nullptr); ios::sync_with_stdio(false); int N; ll M; cin >> N >> M; vector<ll> A(N); for (int i = 0; i < N; i++) { cin >> A[i]; } vector<ll> B(N); for (int i = 0; i < N; i++) { cin >> B[i]; } cout << solve(N, M, A, B) << newl; return 0; }
コメント
testerさんに解いてもらった結果、大量の嘘解法と別解が飛び交ってかなり焦りました。 別解の中にはwriterの想定解より効率の良い解法もありました。
自力でACできた方も他の解法を考えてみると面白いかもしれません。