JOISC 2019 day2 两道料理
作者:互联网
建模转化+维护差分序列
Statement
厨师比太郎正在参加一个厨艺比赛。在这场比赛中参赛者要烹饪两道料理:IOl盖饭和JOI咖喱。
l0I盖饭的烹饪过程中需要N个步骤。第i(1≤i≤N)步的用时是 \(A_i\) 分钟,最初他只能进行第1步,想要进行第i(2≤i≤N)步的条件是已经完成了第i―1步。
JOI咖喱的烹饪过程中需要M个步骤。第j(1≤j≤M)步的用时是 \(B_i\)分钟,最初他只能进行第1步,想要进行第i(2≤j ≤ M)步的条件是已经完成了第j-1步。
做料理过程中需要专心致志,所以当他开始进行一个步骤时,就不能中断。当完成了一个步骤,他也可以选择进行另一道料理的下一个步骤。比赛开始后,在两道料理都完成之前,他不能停下来休息。
在这场比赛中,参赛者会按照接下来的方式得到艺术感的打分:
- 如果他在比赛的前S(1≤i≤N)分钟内完成了IOI盖饭的第à个步骤,那么从中他会得到P;点的分数,分数有可能是负的。
- 如果他在比赛的前T(1≤j≤M)分钟内完成了JOI咖喱的第j个步骤,那么从中他会得到Qj点的分数,分数有可能是负的。
请你帮助比太郎设计做料理过程,最大化他做料理的艺术感评分。
\(n,m \le 10^6\)
Solution
很有意思的一道题!
naive :显然,有用的时刻可以表示为 \((x, y)\) 表示做了前 \(x\) 道 \(A\), 前 \(y\) 道 \(B\),此时,每一个加分项也可以表示成 \((\le x, \le y)\) 之类的形式。这时候,wyb 突然脑洞,联想到 [ARC101D]Robots and Exits 的建模方式,即看作是从 \((0,0)\) 走到 \((n, m)\) ,每步向上向右,然后越过某一条线会有贡献。
对应到这里,就成了 \(x,y\) 轴上分别竖立这若干根和这根坐标轴垂直的线,点经过这条线就会有贡献。
然后感觉这个两种贡献很难受,不好搞,需要找个方式转化一下....(没有时间思考了/fn/fn/fn 思考时间给得太少)
yysy,想到这里,可以称之为高光时刻了 /hanx
正解:平凡地想到这个建模方式的思路应该是设 \(f(x, y)\) 表示做 \(x\) 道 \(A\), 前 \(y\) 道 \(B\) 的最大价值,然后通过对 DP 转移方程的观察看出是一个在二维平面上行走的问题,再把方程中的转移条件转化。
最终得到的模型:从 \((0,0)\) 开始往 \((n,m)\) 每步向上向右,平面上撒着若干个点,对于一个红点 \((i, y_i)\),如果在路径上或者路径上方,有 \(p_i\) 贡献;对于一个蓝点 \((x_i,i)\) ,如果在路径上或者路径下方,有 \(q_i\) 贡献。
注意路径上指的是被路径覆盖。
不好算,简单容斥,先给答案加上 \(\sum p\), 然后把 \(p_i\) 取负。此时需要算的是在路径下方的红点和在路径上/路径下方的蓝点,不统一。
考虑把红点 向左向上整体平移, 那么此时的在路径上/下方 就对应原来的在路径下方
此时,设 \(f(x, y)\) 表示走到 \(x,y\) 的最大贡献,设 \(sum(x,y)\) 表示 \((x,0\dots y)\) 的总贡献,有
\[f(x,y) = \max(f(x, y - 1), f(x - 1, y) + sum(x - 1, y)) \]即我们认为拐角处才贡献,不会算重。注意这里 \(sum\) 不一定单调
竭诚观察这个式子, 思考 \(f\) 的值之间的关系。可以看做是先把 \(f(x - 1)\) 贺了一份过来,此时 \(f(x)\) 是单调的。然后在前缀的某些点上挂上一些标记表示对从 \(i\) 开始的所有值加上一个可正可负的数。
既然是单调的,我们可以以差分的视角来看待这个问题 。原 f(x - 1) 差分数组非负。
考虑当前如果加入的是一个正数,会把差分数组上这个位置增加
当前如若加入的是一个负数,对于差分数组当前数 \(d[i]\) 而言,如果 \(d[i] + x < 0\) 被干成了负数,因为对 \(f(x, y - 1)\) 取了 \(\max\),所以变成 \(0\);而 \(i + 1\) 位置相当于 \(i - 1\) 的增量是 \(d[i] + d[i + 1]\) ,所以是比较 \(d[i]+d[i+1]\) 和 \(x\)。
所以加入一个负数会使得后面连续的一段总和最多为 \(x\) 的数清零 (最后所有数都为 \(0\) 或有一个数只被减去了一部分)。这个过程可以直接用 set/线段树二分 维护。
更顺畅地得到这个维护差分做法的思路是先考虑暴力的做法
暴力做法需要支持的是区间加、所有数取前缀 max
假设这两种操作是交替的,考虑每次把一段区间 \([l,r]\) 整体抬高 \(h\),此时由于前缀 max, 需要二分出第一个位置和 \(a[r]\) 的差值大于 \(h\) ;把一段区间整体降低 \(h\) ,那需要二分出来的是那个位置不需要向前面取 \(\max\)
由此,发现我们非常关注差值的变化,所以我们考虑维护差分数组。
由此,引出了线段树上二分的思路。
一共写 + 调 接近 4h,最开始不懂这个怎么才能 set, 然后自己口胡了一个线段树上二分上去,觉得非常科学
然后狂暴乱肝,狂暴输出+画图手模,2h30min 左右总算是过了样例,交上去 49pts, WA
写了一个暴力对拍,总算是发现了问题 /hanx 写完回头看了一眼 set 的写法,发现其实很好懂而且好写/qd
bugs:
lj 编译器不支持 fread- 线段树上找到查询区间进一步往里走的时候,不要忘记 pushdown
- query 区间和的时候,写法中存在 \(l, r\) 和 \([L,R]\) 无交的情况,死循环
- 把 \((i,y_i) \to (i - 1, y_i + 1)\) 之后, 可能会大于 \(m\)
- 对于 \(x_i = n\) 的情况,必须贡献 (卡死我了
Code
#include<bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fi first
#define se second
#define ls rt << 1
#define rs rt << 1 | 1
#define mid ((l + r) >> 1)
using namespace std;
const int N = 1e6 + 5;
const int inf = 1e9;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1 ++)
int read() {
int s = 0, w = 1; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') w = -1; ch = getchar(); }
while(isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return s * w;
}
struct Tree {
int sum, tg;
} t[N << 2];
int a[N], s[N], p[N];
int b[N], r[N], q[N];
vector<pii> vec[N];
int n, m, cnt, ans;
bool cmp(pii x, pii y) {
return x.fi == y.fi ? x.se < y.se : x.fi > y.fi;
}
void pushup(int rt) {
t[rt].sum = t[ls].sum + t[rs].sum;
}
void pushdown(int rt) {
if(t[rt].tg == 1) {
t[ls].sum = 0, t[ls].tg = 1;
t[rs].sum = 0, t[rs].tg = 1;
t[rt].tg = 0;
}
}
void alter(int l, int r, int rt, int id, int v) {
if(l == r) return t[rt].sum += v, void();
pushdown(rt);
if(id <= mid) alter(l, mid, ls, id, v);
else alter(mid + 1, r, rs, id, v);
pushup(rt);
}
void reset(int l, int r, int rt, int L, int R) {
if(L > R || l > R || r < L) return ;
if(L <= l && r <= R)
return t[rt].sum = 0, t[rt].tg = 1, void();
pushdown(rt);
if(L <= mid) reset(l, mid, ls, L, R);
if(mid < R) reset(mid + 1, r, rs, L, R);
pushup(rt);
}
int query(int l, int r, int rt, int L, int R) {
if(L > R || l > R || r < L) return 0;
if(L <= l && r <= R) return t[rt].sum; pushdown(rt);
if(R <= mid) return query(l, mid, ls, L, R);
if(L > mid) return query(mid + 1, r, rs, L, R);
return query(l, mid, ls, L, R) + query(mid + 1, r, rs, L, R);
}
int binary(int l, int r, int rt, int L, int R, int pre, int v) {
if(L <= l && r <= R) {
if(l == r) {
if(pre + t[rt].sum >= v)
return l;
return inf;
}
pushdown(rt); ////////// Segment Tree stikes me!
if(t[ls].sum + pre >= v)
return binary(l, mid, ls, L, R, pre, v);
return binary(mid + 1, r, rs, L, R, pre + t[ls].sum, v);
}
pushdown(rt);
int tmp = inf;
if(L <= mid) tmp = min(tmp, binary(l, mid, ls, L, R, pre, v));
if(mid < R) tmp = min(binary(mid + 1, r, rs, L, R, pre + query(l, mid, ls, L, R), v), tmp);
return tmp;
}
signed main() {
// freopen("data.in", "r", stdin);
n = read(), m = read();
for(int i = 1; i <= n; ++ i)
a[i] = read() + a[i - 1], s[i] = read() - a[i], p[i] = read();
for(int i = 1; i <= m; ++ i)
b[i] = read() + b[i - 1], r[i] = read() - b[i], q[i] = read();
for(int i = 1; i <= n; ++ i) if(s[i] >= 0) {
int pos = upper_bound(b + 1, b + 1 + m, s[i]) - b;
if(pos <= m) vec[i - 1].push_back(pii(- p[i], pos)); // the check is necessory, as it's illegal
ans += p[i];
}
for(int i = 1; i <= m; ++ i) if(r[i] >= 0)
vec[upper_bound(a + 1, a + 1 + n, r[i]) - a - 1].push_back(pii(q[i], i));
for(int i = 0; i < n; ++ i) {
sort(vec[i].begin(), vec[i].end(), cmp);
for(auto v : vec[i])
if(v.fi >= 0)
alter(0, m, 1, v.se, v.fi); // the upper border is m instead of m + 1 !!!
else {
int r = binary(0, m, 1, v.se, m, 0, -v.fi);
if(r == inf) reset(0, m, 1, v.se, m);
else alter(0, m, 1, r, v.fi + query(0, m, 1, v.se, r - 1)),
reset(0, m, 1, v.se, r - 1);
// 先 alter 再 reset 懂不懂啊
}
}
ans += t[1].sum;
for(auto v :vec[n]) ans += v.fi;
printf("%lld\n", ans);
return 0;
}
/*
4 3
2 1 1
3 8 1
2 13 1
1 13 1
3 6 1
2 11 1
2 15 1
*/
标签:rt,return,int,sum,day2,JOISC,2019,fi,se 来源: https://www.cnblogs.com/wyb-sen/p/16537916.html