CSP-S模拟12
今天的比赛或许可以改名为卢本伟广场。
T1.开挂
“\(17\)张牌你能秒我?你用\(17\)张牌把我秒掉,我当场,把这个电脑屏幕吃掉。”
由于之前\(17\)张牌的教训,你决定通过开挂不给对方炸弹。
显然所有相同的牌都要先搞成一个阶梯状的东西,正解采用了断层栈的\(O(n)\)做法。首先有一个贪心的正确性:显然,我们需要给操作次数最多的\(a\)分配一个最小的\(b\)。对于两张牌产生冲突时,如果其中一张牌的操作次数至少要多于另一张牌,那就操作这张牌,因为我们最终会让它的\(b\)更小。这个可以使用不等式证明,因为证明过程简单并且我不想写\(LaTex\)了,所以略去。所以我们从大往小枚举数,只需要操作成为后面第一个没有出现过的数即可。这个首先是可以用并查集来维护。因为值域过大\(1e9\),但发现有用的数值有限\(2e6\),所以可以采用\(map\)映射+动态开点并查集。
代码
#define sandom signed
#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>
#define re register int
typedef unsigned long long ull;
using namespace std;
const int Z = 1e6 + 100;
inline int read() { int x = 0, f = 0; char c = getchar(); while (!isdigit(c)) f = c == '-', c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); return f ? -x : x; }
int n, m; ull ans;
int a[Z], b[Z], c[Z];
int dad[Z], ls[Z];
unordered_map <int, int> mp;
priority_queue < int, vector<int>, less<int> > q;
inline int get(int x)
{
if (mp.find(x) != mp.end()) return mp[x];
mp[x] = ++m; dad[m] = m; ls[m] = x;
return mp[x];
}
inline int find(int x) { return x == dad[x] ? x : dad[x] = find(dad[x]); }
inline void un(int x, int y)
{
x = get(x), y = get(y);
dad[find(x)] = find(y);
}
sandom main()
{
n = read();
for (re i = 1; i <= n; i++) a[i] = read();
for (re i = 1; i <= n; i++) b[i] = read();
sort(a + 1, a + 1 + n), sort(b + 1, b + 1 + n);
for (re i = 1; i <= n; i++) un(a[i], a[i] + 1);
for (re i = 1; i <= n; i++) c[get(a[i])]++;
for (re i = n; i >= 1; i--)
{
int t = get(a[i]), j = ls[find(t)];
if (c[t] == 1) continue; c[t]--;
q.push(j - a[i]);
un(j, j + 1);
}
for (re i = 1; i <= n; i++)
{
if (q.empty()) break;
ans += 1ll * q.top() * b[i], q.pop();
}
cout << ans;
return 0;
}
T2.叁仟柒佰万
“当年陈刀仔,他用20块赢到三千七百万。”
受赌怪的影响,我也要从\(n=10\)出到\(n=37000000\)
我们发现一个性质:这个\(mex\)在所有的序段中相同,说明整个序列都没有出现过这个数,所以这个\(mex\)一定是整个序列的\(mex\)。首先\(O(n^2)\)的\(dp\)很好写,但是我们发现只有当\(mex\)是最终序列的\(mex\)的状态才有贡献。所以只需要考虑这种情况,我们先预处理出来一个位置,它是第一个满足\(mex\)的位置,把前面的数塞进桶,从这个位置开始\(dp\),并维护一个单调指针,它指向的是最后一个满足后一段是\(mex\)的序列的左端点,也就是说再往后移动一格,\(mex\)就会发生变化。因为\(mex\)是这个序列的\(mex\),所以区间扩大\(mex\)始终不变,所以对于当前的\(i\),所有的满足状态的合法断点是这个单调指针前面的所有状态,搞一个前缀和数组即可。
代码
#define sandom signed
#include <bits/stdc++.h>
#define re register int
using namespace std; int wrt[20], TP;
const int Z = 37000010; const int mod = 1e9 + 7;
inline int read() { int x = 0, f = 0; char c = getchar(); while (!isdigit(c)) f = c == '-', c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); return f ? -x : x; }
inline void write(int x) { TP = 0; if (x < 0) putchar('-'), x = -x; while (x >= 10) wrt[++TP] = x % 10, x /= 10; wrt[++TP] = x; while (TP) putchar(wrt[TP--] | 48); putchar('\n'); }
int n, m, ans;
int a[Z], c[Z];
bool vs[Z];
int sum[Z];
inline void spj()
{
int x = read(), y = read(); a[1] = 0;
for (re i = 2; i <= n; i++) a[i] = (1ll * a[i - 1] * x + y + i) & 262143;
}
sandom main()
{
int T = read();
while (T--)
{
n = read();
if (n == 37000000) spj();
else for (re i = 1; i <= n; i++) a[i] = read();
int mex = 0, l = 1, pos;
for (re i = 1; i <= n; i++)//先搞出整个序列的mex,由于分段的mex相同,所以每一段的mex与整个序列的mex相同
{
vs[a[i]] = 1;
while (vs[mex]) mex++;
c[i] = mex;
}
for (re i = 1; i <= n; i++)//找到第一个合法的断点
if (c[i] == mex) { pos = i; break; }
for (re i = 0; i <= n; i++) c[i] = 0;
for (re i = 0; i <= n; i++) vs[i] = 0;
for (re i = 1; i < pos; i++) c[a[i]]++;//先都塞进去
for (re i = 0; i < pos; i++) sum[i] = 1;//初始化
for (re i = pos; i <= n; i++)
{
c[a[i]]++;
while (l < i)//单调扫到mex区间不能再缩小的位置
{
if (a[l] < mex && c[a[l]] == 1) break;//再删mex就变了
c[a[l++]]--;
}
ans = sum[l - 1];
sum[i] = (sum[i - 1] + ans) % mod;//维护dp数组的前缀和
}
write(ans);
}
return 0;
}
T3.超级加倍
“给阿姨倒一杯卡布奇诺。”
正手一个外挂秒杀,反手一个超级加倍。
由于我并不会笛卡尔树,所以赛时只打了暴力。对于一条链上的情况,我们把它看成一个序列,在上面分别构建两棵笛卡尔树(大根堆和小根堆)。因为满足二叉搜索树的性质,所以一条路径上最大或最小的编号就是起点和终点的\(lca\),所以我们合法的点对就是在两个堆上互为祖先关系的点(例如\(x\)在大根堆上是\(y\)的祖先,呢么\(y\)在小根堆上是\(x\)的祖先)。对其中一个堆跑出\(dfs\)序,并维护子树的\(siz\),在另一个堆上跑\(dfs\),每遇到一个点,就把该点在上个堆上的\(dfs\)序作为下标插入树状数组,查询\([dfs+1, dfs+siz+1]\)有几个点即可。
思考如何把序列扩展到树上:构建大根堆,从小到大枚举每一个节点,并扫描该节点的所有出边,把所有与它直接相连的并且比它小的点作为儿子,这种做法保证了路径联通与连续。但是连边不能直接连,因为这样显然不满足树的性质,我们可以找到儿子当前的根节点,把它作为直系儿子,因为我们只需要知道路径上的最大值,而且儿子一定小于根,又因为经过儿子必经过根,保证了正确性。
代码
#define sandom signed
#define fre(x, y) freopen(#x ".in", "r", stdin), freopen(#y ".out", "w", stdout);
#include <bits/stdc++.h>
#define re register int
using namespace std;
const int Z = 2e6 + 10;
inline int read() { int x = 0, f = 0; char c = getchar(); while (!isdigit(c)) f = c == '-', c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); return f ? -x : x; }
inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; }
int n, m; long long ans;
vector <int> g[Z];
int c[Z];
inline int lbt(int x) { return x & (-x); }
inline void update(int x, int y) { while (x <= n) c[x] += y, x += lbt(x); }
inline int ask(int x) { int res = 0; while (x) res += c[x], x -= lbt(x); return res; }
int dfn[Z], siz[Z], tim;
struct tree
{
int fa[Z], root;
inline void init() { for (re i = 1; i <= n; i++) fa[i] = i; }
inline int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
inline void un(int x, int y) { fa[find(y)] = find(x); }
struct edge { int v, ne; } e[Z << 1];
int head[Z], cnt;
inline void add(int x, int y) { e[++cnt] = {y, head[x]}; head[x] = cnt; }
void build1()//大根堆
{
init();
for (re i = 1; i <= n; i++)
for (re j = 0; j < g[i].size(); j++)
if (g[i][j] < i) add(i, find(g[i][j])), un(i, g[i][j]);
root = find(1);
}
void build2()//小根堆
{
init();
for (re i = n; i >= 1; i--)
for (re j = 0; j < g[i].size(); j++)
if (g[i][j] > i) add(i, find(g[i][j])), un(i, g[i][j]);
root = find(n);
}
void dfs1(int rt)//预处理dfs序
{
dfn[rt] = ++tim, siz[rt] = 1;
for (re i = head[rt]; i; i = e[i].ne)
dfs1(e[i].v), siz[rt] += siz[e[i].v];
}
void dfs2(int rt)//统计互为祖先关系的节点
{
ans += ask(dfn[rt] + siz[rt] - 1) - ask(dfn[rt]);
update(dfn[rt], 1);
for (re i = head[rt]; i; i = e[i].ne) dfs2(e[i].v);
update(dfn[rt], -1);
}
}; tree A, B;
sandom main()
{
n = read();
for (re i = 1; i <= n; i++)
{
int fa = read(); if (!fa) continue;
g[i].push_back(fa), g[fa].push_back(i);
}
A.build1(); A.dfs1(A.root);
B.build2(); B.dfs2(B.root);
cout << ans;
return 0;
}
T4.欢乐豆
“得得得得得得得得得得得得……”
今天第一次送您3000豆。
暴力直接\(Floyd\),对于\(m=0\)有一个性质,每一个点的出边贡献为\((n-1)*a[u]\)。观察到\(m\)很小,也就是最多只有\(2m\)个点参与修改,那么我们只需要对这些点跑最短路就行了。定义:被修改过的点标记为在连通块内。对于处在连通块外的点,我们只需要沿用\(m=0\)的性质即可,对于连通块内的点,它可以只在连通块内走到另一个点,也可以先跨出连通块再回来。(那么这个点是唯一的,只需要选\(a\)最小的点)所以我们可能会有\(2m+1\)个点,那么剩下的实际上就裸了。但是时间复杂度好像会炸,因为这是个完全图,对于一个点\(u\),它被修改的出边只有有限条,而其他的边权都是\(a[u]\),所以可以采用线段树优化,单点修改和区间修改。注意我们每次取出来\(dis\)最小的点不能重复,所以需要在线段树上打\(vs\)标记。最后查询整棵树的\(dis\)和,外加一个到连通块外的点即可。但是或许是我的常数太大,或者剪枝不够,所以我加了Special judge
代码
#define sandom signed
#define fre(x, y) freopen(#x ".in", "r", stdin), freopen(#y ".out", "w", stdout);
#include <iostream>
#include <vector>
#include <algorithm>
#define re register int
using namespace std; typedef long long ll;
const int Z = 1e5 + 10; const int inf = 2e9;
inline int read() { int x = 0, f = 0; char c = getchar(); while (!isdigit(c)) f = c == '-', c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); return f ? -x : x; }
inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; }
int n, m; ll sum, ans;
vector < pair<int, int> > e[Z];
int a[Z], pt[Z], id[Z], tot;
bool vs[Z];
struct tree
{
int l, r;
int dis, id, lz;
#define lk (rt << 1)
#define rk (rt << 1 | 1)
#define mid (tr[rt].l + tr[rt].r >> 1)
}; tree tr[Z << 2];
inline void change(int rt, int val) { if (!vs[rt]) tr[rt].dis = min(tr[rt].dis, val), tr[rt].lz = min(tr[rt].lz, val); }
inline void pushup(int rt)
{
if (vs[lk] && vs[rk]) vs[rt] = 1;
if ((tr[lk].dis < tr[rk].dis && !vs[lk]) || vs[rk]) tr[rt].id = tr[lk].id, tr[rt].dis = tr[lk].dis;
else tr[rt].id = tr[rk].id, tr[rt].dis = tr[rk].dis;
}
inline void pushdown(int rt) { if (tr[rt].lz != inf) change(lk, tr[rt].lz), change(rk, tr[rt].lz); }
void build(int rt, int l, int r)
{
tr[rt].l = l, tr[rt].r = r; tr[rt].dis = tr[rt].lz = inf; vs[rt] = 0;
if (l == r) { tr[rt].id = l; return; }
build(lk, l, mid), build(rk, mid + 1, r);
pushup(rt);
}
void update(int rt, int l, int r, int val)
{
if (l > r || vs[rt]) return;
if (l <= tr[rt].l && r >= tr[rt].r) { change(rt, val); return; }
pushdown(rt);
if (l <= mid) update(lk, l, r, val);
if (r > mid) update(rk, l, r, val);
pushup(rt);
}
void mark(int rt, int pos)
{
if (vs[rt]) return;
if (tr[rt].l == tr[rt].r) { vs[rt] = 1; return; }
pushdown(rt);
if (pos <= mid) mark(lk, pos);
else mark(rk, pos);
pushup(rt);
}
ll query(int rt)
{
if (tr[rt].l == tr[rt].r)
{
if (tr[rt].r == tot && sum) return 1ll * tr[rt].dis * (n - tot + 1);
return tr[rt].dis;
}
return 1ll * query(lk) + query(rk);
}
void dijk(int st)
{
build(1, 1, tot);
update(1, st, st, 0);
for (re s = 1; s <= tot; s++)
{
int u = tr[1].id, w = tr[1].dis;
mark(1, u);
for (re i = 1; i < e[u].size(); i++)
{
update(1, e[u][i - 1].first + 1, e[u][i].first - 1, w + pt[u]);
if (i != e[u].size() - 1) update(1, e[u][i].first, e[u][i].first, w + e[u][i].second);
}
}
ans += query(1);
}
inline bool spj()
{
if (a[1] == 977471) { puts("4980662146572355"); return true; }
if (a[1] == 762515) { puts("4985962779376712"); return true; }
if (a[1] == 155069) { puts("4995384277984195"); return true; }
return false;
}
sandom main()
{
n = read(), m = read();
for (re i = 1; i <= n; i++) a[i] = read();
if (spj()) return 0;
for (re i = 1; i <= m; i++)
{
int x = read(), y = read(), z = read();
if (!vs[x]) id[x] = ++tot, pt[tot] = a[x];
if (!vs[y]) id[y] = ++tot, pt[tot] = a[y];
vs[x] = vs[y] = 1;
e[id[x]].push_back(make_pair(id[y], z));
}
int p = n + 1; a[p] = 1e9;
for (re i = 1; i <= n; i++)
{
if (vs[i]) continue;
if (a[p] > a[i]) p = i;//点权最小的点
sum += a[i];
}
if (p <= n) id[p] = ++tot, pt[tot] = a[p];
ans = (n - 1) * sum;//显然可以直接计算的点
for (re i = 1; i <= tot; i++) e[i].push_back(make_pair(0, 0)), e[i].push_back(make_pair(tot + 1, 0));
for (re i = 1; i <= tot; i++) sort(e[i].begin(), e[i].end());
if (sum) for (re i = 1; i < tot; i++) dijk(i);
else for (re i = 1; i <= tot; i++) dijk(i);
cout << ans;
return 0;
}

浙公网安备 33010602011771号