贪心随笔
贪心
自己中の光線銃 乱射する 強者のナンセンス
オートクチュールで作る 殺しのライセンス
分断を生んじゃった椅子取りゲーム 無痛分娩で授かるベイブ
壮大な内輪ノリを歴史と呼ぶ
选取局部最优解达到全局最优解的算法。
贪心的正确性需要证明,有如下证明方法:
- 微扰
- 范围缩放
- 决策包容
- 反证
- 数学归纳
贪心应用
P1561 [USACO12JAN] Mountain Climbing S
是一道普通贪心题。
贪心策略是使有牛下山时尽可能使山上有牛。
需要将牛分为 \(U(i) \ge D(i)\) 和 \(U(i) \lt D(i)\) 两种情况,让第二类牛先上能满足贪心。
其他比较简单。时间复杂度 \(\mathcal{O}(n)\)。
code
const int N = 2.5e4 + 5;
struct node { int up, dw; } a[N];
int n, ut[N], dt[N];
bool cmp(node a, node b) {
if (a.up < a.dw) {
if (b.up < b.dw) return a.up < b.up;
else return 1;
} else {
if (b.up < b.dw) return 0;
else return a.dw > b.dw;
}
}
int main() {
read(n);
rep(i, 1, n) read(a[i].up, a[i].dw);
sort(a + 1, a + n + 1, cmp);
rep(i, 1, n) ut[i] = ut[i - 1] + a[i].up;
rep(i, 1, n) dt[i] = max(dt[i - 1], ut[i]) + a[i].dw;
write(dt[n]);
return 0;
}
P11323 【MX-S7-T1】「SMOI-R2」Happy Card
\(\color{#39C5BB}{[EASY]}\)
需要观察到“炸”是三带一,即三张相同的牌可以带任意一张牌。
贪心地尽可能出三带一是对的,这点在模拟出牌后即可发现。
然后将牌分成 \(3\) 个一组,\(2\) 个一组,单独一组,分别讨论答案就好了。
code
const int N = 3e5 + 5;
int T, n, v[N];
i64 cnt[4], ans;
void init() {
cnt[1] = cnt[2] = cnt[3] = 0;
ans = 0;
}
void solve() {
read(n); init();
rep(i, 1, n) read(v[i]);
rep(i, 1, n) {
cnt[3] += v[i] / 3;
if (v[i] % 3) cnt[v[i] % 3]++;
}
if (cnt[3] <= cnt[1]) return write(cnt[3] + (cnt[1] - cnt[3]) + cnt[2], '\n'), void();
if (cnt[3] - cnt[1] <= 2 * cnt[2]) return write(cnt[3] + (2 * cnt[2] - (cnt[3] - cnt[1]) + 1) / 2, '\n'), void();
ans += cnt[2] * 2 + cnt[1];
cnt[3] -= (2 * cnt[2] + cnt[1]);
ans += (cnt[3] / 4) * 3; cnt[3] %= 4;
if (cnt[3] == 3) ans += 3;
else if (cnt[3] == 2 || cnt[3] == 1) ans += 2;
write(ans, '\n');
}
int main() {
read(T);
while (T--) solve();
return 0;
}
P12026 [USACO25OPEN] Compatible Pairs S
\(\color{#39C5BB}{[EASY+]}\)
Sol 1
考虑向 \(A - id(i), B - id(i)\) 连边,注意 \(A = B\)。
发现形成的图没有除了自环以外的环,且每个节点的度数只能为 \(1\) 或 \(2\)。
几乎没见有人写过的证明
如果环长为奇数,会有一条边两个端点的和即被要求 \(A\) 和 \(B\),这是不可能的。
如果环长为偶数,发现从一个点出发两边系数 \(A, B\) 的系数恰好相反,则通过移项可以得到 \(A = B\)。
这样,对这个无向图遍历一遍计算答案即可,需要注意的是,自环要后跑,原因显然。
code
const int M = 2e5 + 10;
int n, a, b, val[M], d[M], in[M];
map<int, int> mp;
vector<int> e[M];
queue<int> que;
i64 ans;
int main() {
read(n, a, b);
rep(i, 1, n) read(val[i], d[i]), mp[d[i]] = i;
rep(i, 1, n) {
if (d[i] <= a && mp[a - d[i]]) e[i].push_back(mp[a - d[i]]), in[mp[a - d[i]]]++;
if (a == b) continue;
if (d[i] <= b && mp[b - d[i]]) e[i].push_back(mp[b - d[i]]), in[mp[b - d[i]]]++;
}
rep(i, 1, n) if (in[i] == 1) que.push(i);
while (que.size()) {
int u = que.front(); que.pop();
for (auto v : e[u]) {
if (v == u) {
ans += val[v] / 2, val[v] %= 2;
continue;
}
if (val[v]) {
int res = min(val[v], val[u]);
ans += res, val[v] -= res, val[u] -= res;
que.push(v);
}
}
}
write(ans);
return 0;
}
Sol 2
贪心。将奶牛群按 \(id\) 从小到大排序,先和 \(B - id\) 匹配,再和 \(A - id\) 匹配,正确性显然。注意一下自己与自己匹配就做完了。
code
const int N = 2e5 + 5;
struct node { int n, id; } c[N];
int n, a, b;
i64 ans;
map<int, int> mp;
int main() {
read(n, a, b);
rep(i, 1, n) read(c[i].n, c[i].id), mp[c[i].id] = c[i].n;
sort(c + 1, c + n + 1, [&](node a, node b) { return a.id < b.id; });
rep(i, 1, n) {
if (c[i].id <= b && mp[b - c[i].id]) {
if (2 * c[i].id == b) ans += mp[c[i].id] / 2, mp[c[i].id] %= 2;
else {
int x = min(mp[c[i].id], mp[b - c[i].id]);
ans += x, mp[c[i].id] -= x, mp[b - c[i].id] -= x;
}
}
if (c[i].id <= a && mp[a - c[i].id]) {
if (2 * c[i].id == a) ans += mp[c[i].id] / 2, mp[c[i].id] %= 2;
else {
int x = min(mp[c[i].id], mp[a - c[i].id]);
ans += x, mp[c[i].id] -= x, mp[a - c[i].id] -= x;
}
}
}
write(ans);
return 0;
}
反悔贪心
不需要选最优解,从一个较劣的情况变为最优解。
P1484 种树
反悔贪心板子。
先贪心选最大,然后做一个用于反悔的坑。
变成链表上操作,方便模拟题意。
时间复杂度 \(\mathcal{O}(n\log n)\)。
code
const int N = 6e5 + 5;
int n, k, a[N];
int pre[N], Next[N];
priority_queue<pii> que;
bool vis[N];
i64 ans;
int main() {
read(n, k);
rep(i, 1, n) read(a[i]);
rep(i, 2, n) pre[i] = i - 1;
rep(i, 1, n - 1) Next[i] = i + 1;
rep(i, 1, n) que.push({a[i], i});
while (k) {
while (vis[que.top().second]) que.pop();
int val = que.top().first, pos = que.top().second;
if (val < 0) break;
int pval = a[pre[pos]], nval = a[Next[pos]];
a[++n] = pval + nval - val; que.push({a[n], n});
ans += val, vis[pos] = 1, vis[pre[pos]] = 1, vis[Next[pos]] = 1;
Next[pre[pre[pos]]] = n, pre[n] = pre[pre[pos]];
Next[n] = Next[Next[pos]], pre[Next[Next[pos]]] = n;
k--;
}
write(ans);
return 0;
}
AGC018 C Coins
开始随便选,然后交换即可。
用六个堆维护每个人换成另外两种币的收益。
显然会有两两互换或三人互换的情况。容易证明多人互换的情况可以用前两种情况表示。
容易证明时间复杂度是 \(\mathcal{O}(n\log n)\)。code。
P11268 【MX-S5-T2】买东西题
\(\color{#FFA500}{[NORMAL-]}\)
Sol 1
50 pts 简单,考虑每个优惠券会对那些商品起作用。
发现作用区间为一段后缀,作用形如替换降价价格,考虑用线段树计算答案。
首先将优惠券按限制从大到小排序。然后对于每个优惠券,令它对它起作用区间内降价最少的替换,这样一定不劣。
因为后缀只会变长,这样处理出了局部最优解。
我的错误
考场上觉得需要按降价对优惠券进行排序。
一个很典的问题,局部最优解不是全局最优解。
因为把后面的位置占了,有后效性,而上面的没有。
code
const int N = 1e6 + 5;
struct Node { int a, b; } a[N], b[N];
int n, m, c[N], d[N], tr[N << 2];
i64 ans;
void up(int rt) { tr[rt] = d[tr[ls]] > d[tr[rs]] ? tr[rs] : tr[ls]; }
void build(int rt, int l, int r) {
if (l == r) return tr[rt] = l, void();
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
up(rt);
}
void update(int rt, int l, int r, int p, int v) {
if (l == r) return d[p] = v, void();
int mid = (l + r) >> 1;
if (p <= mid) update(ls, l, mid, p, v);
else update(rs, mid + 1, r, p, v);
up(rt);
}
int query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return tr[rt];
int mid = (l + r) >> 1, pos, val = 1e9 + 5;
if (L <= mid) {
int p = query(ls, l, mid, L, R);
if (d[p] < val) pos = p, val = d[p];
}
if (mid < R) {
int p = query(rs, mid + 1, r, L, R);
if (d[p] < val) pos = p;
}
return pos;
}
int main() {
read(n, m);
rep(i, 1, n) read(a[i].a, a[i].b);
rep(i, 1, m) read(b[i].a, b[i].b);
sort(a + 1, a + n + 1, [&](Node a, Node b) { return a.a < b.a; });
sort(b + 1, b + m + 1, [&](Node a, Node b) { return a.a > b.a; });
rep(i, 1, n) c[i] = a[i].a, d[i] = a[i].a - a[i].b;
build(1, 1, n);
rep(i, 1, m) {
int l = lower_bound(c + 1, c + n + 1, b[i].a) - c;
if (l > n) continue;
int p = query(1, 1, n, l, n);
if (d[p] < b[i].b) update(1, 1, n, p, b[i].b);
}
rep(i, 1, n) ans += a[i].a - d[i];
write(ans);
return 0;
}
Sol 2
将使用优惠券看做替换优惠价,最终答案是 \(\sum\limits_{i = 1}^na_i - b_i\)。
由上可知两个用了优惠券的物品交换优惠券不会更优。
将所有物品按原价,优惠券按限制从小到大排序,考虑每个物品的情况。一个物品可以用或不用没用过的优惠券,也可以用之前的物品用过的优惠券。
前面的情况简单,用优先队列处理,考虑后一种。设原来对这两个物品 \(i, j\;(i\lt j)\) 的答案为 \(a_i - v + b_j\),后答案变为 \(b_i + a_j - v\)。答案减小了 \((a_i - b_i)-(a_j - b_j)\)。需要 \((a_i - b_i)\) 尽可能大,和前面一起用优先队列处理。
code
const int N = 1e6 + 5;
struct node { int a, b; } a[N], b[N];
priority_queue<int> que;
int n, m, it = 1;
i64 ans;
int main() {
read(n, m);
rep(i, 1, n) read(a[i].a, a[i].b);
rep(i, 1, m) read(b[i].a, b[i].b);
sort(a + 1, a + n + 1, [&](node a, node b) { return a.a < b.a; });
sort(b + 1, b + m + 1, [&](node a, node b) { return a.a < b.a; });
rep(i, 1, n) {
while (it <= m && b[it].a <= a[i].a) que.push(b[it].b), it++;
if (que.size() && que.top() > a[i].a - a[i].b) ans += a[i].a - que.top(), que.pop(), que.push(a[i].a - a[i].b);
else ans += a[i].b;
}
write(ans, '\n');
return 0;
}
浙公网安备 33010602011771号