题目链接:https://codeforces.com/contest/1320
A - Contest for Robots
好久不见的水题了。读清楚要干什么就可以了。
B - Journey Planning
题意:给一个序列A,从中选取一个上升子序列B,使得B中相邻元素的的差与其在序列A中的下标的差相等。也就是 (i-j=A_i-A_j) 。
题解:观察一下发现存在传递性,一开始考虑用什么并查集做,最后看了一下规律发现序列A中每个元素都减去其下标后,得到的差值相同的元素都是同一个上升子序列中的。严格的证明是:移项得 (A_i-i=A_j-j) ,用个map存以差值为key,元素原本的大小的总和为value,最后遍历一次map即可。
C - Remove Adjacent
题意:给一个字符串,每次操作可以选择一个满足“在字符串中位置相邻的字符是其在字符集中的前一个字符”的字符移除,求最多移除多少个字符。
题解:其实字符集和字符串长度都可以可以扩展到1e6的规模。容易发现每次都从'z'开始尝试移除,因为移除'z'不会更差,假如没有'z'则移除'y'。所以记录每个字符的左边和右边分别是谁,然后整一个优先队列,把当前可以移除的元素都丢进去。容易发现在最优策略下,移除队首元素不会影响已经在优先队列中的元素的可移除性。
char s[200005];
int l[200005];
int r[200005];
bool vis[200005];
priority_queue<pair<char, int> > pq;
void test_case() {
int n;
scanf("%d", &n);
scanf("%s", s + 1);
r[0] = 1;
l[n + 1] = n;
for(int i = 1; i <= n; ++i) {
l[i] = i - 1;
r[i] = i + 1;
if(l[i] >= 1 && s[l[i]] == s[i] - 1) {
pq.push({s[i], i});
vis[i] = 1;
} else if(r[i] <= n && s[r[i]] == s[i] - 1) {
pq.push({s[i], i});
vis[i] = 1;
}
}
int cnt = 0;
while(!pq.empty()) {
int u = pq.top().second;
pq.pop();
++cnt;
r[l[u]] = r[u];
l[r[u]] = l[u];
if(l[u] >= 1 && vis[l[u]] == 0 && s[l[u]] == s[r[u]] + 1) {
pq.push({s[l[u]], l[u]});
vis[l[u]] = 1;
}
if(r[u] <= n && vis[r[u]] == 0 && s[r[u]] == s[l[u]] + 1) {
pq.push({s[r[u]], r[u]});
vis[r[u]] = 1;
}
}
printf("%d
", cnt);
}
*D - Navigation System
题意:有一个n个点m条边的有向图。给出一条简单路径P(简单路径是指无重复点),起点是s,终点是t。在简单路径P上的每个点时,系统都会计算并给出到t的其中一条最短路,这样有时候系统给出的导航会发生变化,求最少的变化次数和最多的变化次数。
题解:有点像问最短路的多种解法。回忆在dijkstra的时候,先跑出从t出发的最短路,然后再跑一次dijkstra,当时使用某条边松弛最短路时发现松弛前后的距离相等,则这条边在最短路径DAG中。那么在沿着简单路径P走的时候,若走的不是任何一条最短路,则一定会重新计算最短路,否则,假如这个点有大于2种选择,最少的变化次数就是一次就给到了需要的最短路,最多的变化次数就是给了另一条最短路。
vector<int> G[200005];
int path[200005];
bool vis[200005];
int dis[200005];
vector<int> pre[200005];
int cnt[200005];
queue<int> Q;
void bfs(int n, int s) {
for(int i = 1; i <= n; ++i)
dis[i] = -1;
vis[s] = 1;
dis[s] = 0;
cnt[s] = 1;
Q.push(s);
while(!Q.empty()) {
int u = Q.front();
Q.pop();
for(auto &v : G[u]) {
if(!vis[v]) {
vis[v] = 1;
dis[v] = dis[u] + 1;
pre[v].push_back(u);
cnt[v] = 1;
Q.push(v);
} else {
if(dis[v] == dis[u] + 1) {
pre[v].push_back(u);
++cnt[v];
}
}
}
}
/*for(int i = 1; i <= n; ++i) {
printf("i=%d
", i);
for(auto &v : pre[i])
printf(" %d,", v);
printf("
");
}*/
}
bool check(int x, int y) {
for(auto &v : pre[x]) {
if(v == y)
return true;
}
return false;
}
void test_case() {
int n, m;
scanf("%d%d", &n, &m);
while(m--) {
int u, v;
scanf("%d%d", &u, &v);
G[v].push_back(u);
}
int k;
scanf("%d", &k);
for(int i = 1; i <= k; ++i)
scanf("%d", &path[i]);
bfs(n, path[k]);
int minval = 0, maxval = 0;
for(int i = 1; i < k; ++i) {
if(!check(path[i], path[i + 1]))
++minval, ++maxval;
else {
if(cnt[path[i]] >= 2)
++maxval;
}
}
printf("%d %d
", minval, maxval);
}
收获:注意是要求出最短路径DAG,而不是最短路径生成树。
*E - World of Darkraft: Battle for Azathoth
题意:有n种武器,第i种武器拥有攻击力ai和价格cai。有m种盔甲,第j中盔甲拥有防御力bj和价格cbj。有p个怪物,第k个怪物拥有防御力xk,攻击力yk以及击败收益zk。必须选择恰好一种武器和恰好一种盔甲,然后可以击败满足ai>xk且bj>yk的怪物,求最大的收益。
题解:比赛的时候没看出来是个二维偏序。还以为是三维偏序,然后在对x排序然后分治什么的。后面又以为是枚举怪物然后选择最便宜的能击败它的武器和盔甲来做,这样忽视了选取的偏序的矩形是由两个怪物分别限制长和宽的情况(因为这样的矩形不见得全部都以怪物作为顶点)。
正确做法,按x排序,然后用线段树维护y。线段树的每个叶子,就表示持有这个防御力的最大收益,这里可以预处理出获得至少这个防御力的最小花费。然后因为已经按x排序,所以之前加入线段树的点一定有x不超过当前的x,这样就选攻击力为[x+1,MAXN]的最便宜的武器,盔甲的事情直接询问线段树。
需要注意的是无穷大取LINF才行,或者阻止越界访问。(购买防御力为MAXN+1的盔甲的代价必须远远超过击败所有怪物的收益总和,也就是使得无法购买这套防御力不存在的盔甲)。
const int MAXN = 1000000 + 1;
ll ca[MAXN + 5];
ll cb[MAXN + 5];
struct Monster {
int x, y, z;
bool operator<(Monster &m) {
return x < m.x;
}
} mon[MAXN + 5];
struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
ll ma[(MAXN << 2) + 5];
ll lz[(MAXN << 2) + 5];
void PushUp(int o) {
ma[o] = max(ma[ls], ma[rs]);
}
void PushDown(int o, int l, int r) {
if(lz[o]) {
lz[ls] += lz[o];
lz[rs] += lz[o];
ma[ls] += lz[o];
ma[rs] += lz[o];
lz[o] = 0;
}
}
void Build(int o, int l, int r) {
if(l == r) {
ma[o] = -cb[l];
} else {
int m = (l + r) >> 1;
Build(ls, l, m);
Build(rs, m + 1, r);
PushUp(o);
}
lz[o] = 0;
}
void Update(int o, int l, int r, int ql, int qr, ll v) {
if(ql <= l && r <= qr) {
lz[o] += v;
ma[o] += v;
} else {
PushDown(o, l, r);
int m = (l + r) >> 1;
if(ql <= m)
Update(ls, l, m, ql, qr, v);
if(qr >= m + 1)
Update(rs, m + 1, r, ql, qr, v);
PushUp(o);
}
}
ll QueryMax(int o, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) {
return ma[o];
} else {
PushDown(o, l, r);
int m = (l + r) >> 1;
ll res = -LINF;
if(ql <= m)
res = QueryMax(ls, l, m, ql, qr);
if(qr >= m + 1)
res = max(res, QueryMax(rs, m + 1, r, ql, qr));
return res;
}
}
#undef ls
#undef rs
} st;
void test_case() {
for(int i = 1; i <= MAXN + 1; ++i) {
ca[i] = LINF;
cb[i] = LINF;
}
int n, m, p;
scanf("%d%d%d", &n, &m, &p);
while(n--) {
int ai, cai;
scanf("%d%d", &ai, &cai);
ca[ai] = min(ca[ai], (ll)cai);
}
while(m--) {
int bi, cbi;
scanf("%d%d", &bi, &cbi);
cb[bi] = min(cb[bi], (ll)cbi);
}
for(int i = MAXN; i >= 1; --i) {
ca[i] = min(ca[i], ca[i + 1]);
cb[i] = min(cb[i], cb[i + 1]);
}
st.Build(1, 1, MAXN);
for(int i = 1; i <= p; ++i)
scanf("%d%d%d", &mon[i].x, &mon[i].y, &mon[i].z);
sort(mon + 1, mon + 1 + p);
ll ans = -ca[1] - cb[1];
for(int i = 1; i <= p; ++i) {
st.Update(1, 1, MAXN, mon[i].y + 1, MAXN, mon[i].z);
ans = max(ans, -ca[mon[i].x + 1] + st.QueryMax(1, 1, MAXN, 1, MAXN));
}
printf("%lld
", ans);
}
收获:
1、检查越界。
2、检查无穷大是否够大。
3、线段树的lazy标记真香,要什么树状数组。