[UOJ NOI Round#6 Day1] 题解
\(trip\)
\(Problem\)
给定一张 \(n\) 个点 \(m\) 条边的图,边有边权。
小 \(H\) 要和 \(k\) 个朋友依次在这张图上会面,第 \(i\) 个朋友初始位置在 \(p_i\),小 \(H\) 一开始在 \(1\) 号点。
当他和一个或多个朋友处于同一位置时,则视为完成了会面。
小 \(H\) 和他的朋友们每秒钟移动的速度均为 \(1\),他想知道,倘若他和朋友按照一个商量好的策略移动,那么他和所有网友顺次会面最少需要花费多少秒?
注意,小 \(H\) 可以在边上折返,也能在边上完成会面。例如,一条长度为 \(1\) 的边连接了 \(x\) 和 \(y\) 两点,小 \(H\) 和一个朋友于 \(t\) 时刻分别处于点 \(x\) 和点 \(y\),小 \(H\) 可以花费 \(0.5s\) 走到边中间,和朋友完成会面,再花费 \(0.5s\) 返回点 \(x\)。
\(Scope\ Limitation\)
\(2\leq n\leq 10^5,n-1\leq m\leq 2\times 10^5,1\leq k\leq 20\),图无重边、自环。
\(Solution\)
注意到一个关键点转化:设 \(f(x) = max\{dis(p_i,x)\}(0\leq i\leq k)\),特殊的,我们令 \(p_0 = 1\)。其中 \(x\) 可以是图上任意一点,这个点可以在边上,\(dis(u,v)\) 表示图上任意两点间的距离。而题目实际上是让我们求 \(f(x)\) 的最小值。
证明实际上也相当简单,即考虑和某个朋友相遇后,让该朋友继续跟着小 \(H\) 移动,最后所有人一定会移动到同一个点,而我们的答案只和最后一个到达该点的人有关。
我们可以在 \(\mathcal O(knlogn)\) 的时间复杂度内处理出所有人到图上节点的距离,但这样我们仍然不能得出答案,因为最后相聚的点可能在边上。
略作思考,我们发现实际上对于一条两端点分别为 \(x\) 和 \(y\) 的边,令一些人到达节点 \(x\),再令一些人到达节点 \(y\),分别处理出到达 \(x\) 和 \(y\) 的人中花费时间的最值,就能简单的算出这两人怎么行动会是最优解。
如果直接枚举所有情况,时间复杂度是 \(\mathcal O(2^km+knlogn)\),能够拿到 \(85\) 分。
事实上,我们只需对所有人到达点 \(x\) 花费的时间排序,让一个前缀走 \(x\),一个后缀走 \(y\) 即可。这不难理解,因为我们只需要知道最后到达 \(x\) 和 \(y\) 的人,前缀走 \(x\),可以确定出最后到达 \(x\) 的人,后缀走 \(y\),亦能通过预处理计算出最后到达 \(y\) 的人,且我们一定不需要前缀中的某些数,其不会让答案更优。
时间复杂度 \(\mathcal O(km + knlogn)\)
\(code\)
#include <bits/stdc++.h>
#define st first
#define nd second
#define mk make_pair
#define pii pair<int, int>
#define int long long
using namespace std;
const int N = 2e5 + 10, INF = 1e15;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
struct edge{ int v, w; };
struct record{ int u, v, w; }rec[N];
struct node{
int v, p;
bool operator < (const node &x) const{ return v > x.v; }
};
int n, m, k, ans;
int dis[22][N];
bool vis[N];
vector<edge> G[N];
inline void DJ(int st, int id)
{
priority_queue<node> q;
memset(vis, false, sizeof(vis));
memset(dis[id], 127 / 3, sizeof(dis[id]));
dis[id][st] = 0, q.push((node){0, st});
while(!q.empty()){
node u = q.top(); q.pop();
if(vis[u.p]) continue;
vis[u.p] = true;
for(register edge to : G[u.p]){
if(dis[id][u.p] + to.w < dis[id][to.v]){
dis[id][to.v] = dis[id][u.p] + to.w;
q.push((node){dis[id][to.v], to.v});
}
}
}
}
inline void update(int l, int r, int id)
{
if(l == -1 || r == -1) { ans = min(ans, max(l, r) * 2); return; } //全部聚集在其中一边
int c = abs(l - r), z = rec[id].w; //c 表示时间差,z 表示边长
ans = min(ans, 2 * max(l, r) + max(0ll, z - c)); //先到的一方先走 c 的距离,之后 z - c 的距离一起走
}
signed main()
{
n = read(), m = read(), ans = INF;
for(register int i = 1; i <= m; i++){
int x = read(), y = read(), z = read();
G[x].push_back((edge){y, z}), G[y].push_back((edge){x, z});
rec[i] = (record){x, y, z};
}
k = read();
for(register int i = 1, x; i <= k; i++) x = read(), DJ(x, i);
DJ(1, k + 1);
for(register int i = 1; i <= m; i++){
vector<pii> vec;
int x = rec[i].u, y = rec[i].v;
for(register int j = 1; j <= k + 1; j++) vec.push_back(mk(dis[j][x], j));
sort(vec.begin(), vec.end());
int mx[22];
mx[vec.size()] = -1;
for(register int j = vec.size() - 1, id; j >= 0; j--)
id = vec[j].nd, mx[j] = max(mx[j + 1], dis[id][y]);
update(-1, mx[0], i);
for(register int j = 0; j < (int)vec.size(); j++) update(vec[j].st, mx[j + 1], i);
}
printf("%lld\n", ans);
return 0;
}
\(show\)
\(Problem\)
有一个长度为 \(n\) 的 \(01\) 串 \(S\),你需要计算 \(t\) 次操作后能得到多少不同的 \(01\) 串。
一次操作定义为:在串中选择两个位置插入一对 \(01\) 使得 \(0\) 在 \(1\) 前。
\(Scope\ Limitation\)
\(1\leq n,t\leq 300\)
\(Solution\)
考虑状态压缩。
首先,进行一个转化,注意到插入依次匹配且 \(0\) 必须在 \(1\) 之前十分像括号序列,将 \(0\) 视为左括号,将 \(1\) 视为右括号。
设最后的零一串为 \(T\),\(f_{i,j}\) 表示对于 \(T\) 长度为 \(i\) 的前缀,是否能通过 \(S\) 长度为 \(j\) 的前缀加上若干个通过操作插入的 \(01\) 得到。
则 \(f_{i,j}\) 的转移如下:
- \(T_{i+1} = S_{j+1}:f_{i,j}\rightarrow f_{i+1,j+1}\)
- \(a_{i+1}\geq b_i:f_{i,j}\rightarrow f_{i+1, j}\)
第一个转移是简单的,即 \(T\) 和 \(S\) 直接相匹配。
第二个转移相当于在这个位置插入了一个与 \(S\) 不匹配的括号,我们令 \(a_i\) 和 \(b_i\) 分别表示 \(T\) 和 \(S\) 作为括号序列的权值,即若 \(T_i\) 是左括号,有 \(a_i = a_{i-1} + 1\),否则,\(a_i = a_{i-1}-1\),\(b_i\) 则类似。限制条件实际上即是满足了去除 \(S\) 原有的括号,我们插入的括号必须是一个能够完全匹配的括号序列。
明白了 \(f_{i,j}\) 的转移后,考虑设计状态。
设 \(dp_{i,j,V}\) 表示 \(T\) 填了 \(i\) 位后,\(a_i = j\),且当前状态与 \(S\) 的匹配度压缩为 \(V\) 的方案数。其中,\(V\) 是一个二进制数,第 \(k\) 位表示 \(f_{i,k}\) 的值。
最后按照上述转移计算,最后状态中 \(V\) 第 \(n\) 位为 \(1\) 且 \(j = b_n\) 计入答案。
倘若直接转移,我们的状态数大概是 \(\mathcal O(2^n (n + 2\times t)^2)\),忽略一些无用状态,再进行滚动,同时用 \(\text{map}\) 记录每个状态的值,能够通过 \(45\) 分的部分。
考虑优化我们的状态。
我们发现了这样一个事实,即对于 \(V\),他对应的下两个状态的 \(V'\) 的最高位事实上只和当前 \(V\) 的最高位有关,这是显然的,于是我们只需要记录 \(V\) 的最高位。
但这样存在一个问题,我们现在记录的状态中,省略了低位,但是事实上,我们的最高位有可能无法向后更新,而低位却能。简单来说,还是 \(a_{i+1}\geq b_i\) 的问题,我们能够插入一些左括号,不断叠高 \(a\) 的值。
对此,设计一个反悔操作。
在 \(S\) 上预处理一个 \(pre_i\),记录最大的 \(j\) 满足 \(j < i\) 且 \(b_{j} < b_i\)。
这样做有什么好处?细想我们的转移,很显然能够转移到当前状态,一定是满足 \(a_i\ge b_i\) 的,而若不能转移,只存在一种情况,就是 \(a_i = b_i\),且我们想在 \(i+1\) 这个位置填入一个右括号。这时,我们只需要让 \(S\) 上 \([pre_i + 1,i]\) 这段区间由操作插入负责,就可以降低 \(b_i\),从而实现目标。
同时 \([pre_i + 1,i]\) 一定是可以由插入负责的,因为按照上述定义,若 \(pre_i\) 存在,这一段区间就是一段能够完全匹配的括号序列,或者单个左括号。
这样,状态的数量的极值就变成了 \(\mathcal O(n(n+2\times t) ^ 2)\),这一定是跑不满的,足以通过此题。
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3, M = 3e2 + 10, mod = 998244353;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
//括号序列的值、f 的压缩、该状态对应的数量
struct Status{ int a, v; };
char s[N];
int n, t, opt, cnt;
int b[N], pre[N], dp[2][N << 1][M];
bool vis[2][N << 1][M];
vector<Status> stu[2];
signed main()
{
n = read(), t = read(), cin >> (s + 1), b[0] = 910;
for(register int i = 1; i <= n; i++){
if(s[i] == '0') b[i] = b[i - 1] + 1;
else b[i] = b[i - 1] - 1;
}
for(register int i = 0; i <= n; i++){
pre[i] = -1;
for(register int j = i - 1; j >= 0; j--)
if(b[j] < b[i]) { pre[i] = j; break; }
}
dp[opt ^ 1][910][0] = 1, stu[opt ^ 1].push_back((Status){910, 0});
t = n + 2 * t;
while(t--){
//清空当前状态
for(register Status u : stu[opt]) vis[opt][u.a][u.v] = false, dp[opt][u.a][u.v] = 0;
stu[opt].clear();
for(register Status u : stu[opt ^ 1]){ //枚举上一轮状态
Status to;
//这一轮是左括号
to.a = u.a + 1, to.v = -1;
if(s[u.v + 1] == '0'){
to.v = u.v + 1;
(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
}
else{
if(to.a >= b[u.v]){
to.v = u.v;
(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
}
}
//若这一轮是右括号
to.a = u.a - 1, to.v = -1;
if(s[u.v + 1] == '1'){
to.v = u.v + 1;
(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
}
else{
if(to.a >= b[u.v]){
to.v = u.v;
(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
}
else{
if(pre[u.v] != -1){
(dp[opt][to.a][pre[u.v]] += dp[opt ^ 1][u.a][u.v]) %= mod;
if(!vis[opt][to.a][pre[u.v]]) stu[opt].push_back((Status){to.a, pre[u.v]}), vis[opt][to.a][pre[u.v]] = true;
}
}
}
}
opt ^= 1, cnt++;
}
printf("%lld\n", dp[opt ^ 1][b[n]][n]);
return 0;
}
/*
2 1
00
*/
\(game\)
\(Problem\)
给定一个依次排列长度为 \(n\) 的序列,两个人将在这个序列上做一个游戏:
- 两人依次选择序列里的数,不能选择已经被自己或对方选走的数,小 \(B\) 先手。
- 小 \(A\) 每次只会选择剩下的数中编号最小的那个,小 \(B\) 会自己制定策略。
给定 \(q\) 次询问,每次询问给出两个数 \(l\) 和 \(r\),问在区间 \([l,r]\) 上进行这个游戏,小 \(B\) 选取的数和最大是多少?
\(Scope\ Limitation\)
\(1\leq n,q\leq 2\times 10^5\)
\(Solution\)
依次考虑每个数,我们发现,小 \(B\) 的选取有这样一个限制,对于前 \(i\) 个数,他最多选取 \(\lceil\frac{i}{2} \rceil\) 个。
设计状态 \(f_{i,j}\) 表示前 \(i\) 个数选取了 \(j\) 个的答案,直接转移即可,时间复杂度 \(\mathcal O(n^2q)\),期望得分 \(40\) 分。
仔细想了想这个限制,发现我们向后面加数,并不会影响到前面没有被选择的数,也就是说前面没有被选择的数依然不会被选择,也不会增加选取前面的数的机会。但他有可能可以替换掉被选取的数,因为它在更靠后的位置,这是十分显然的。
于是我们得到了一个反悔贪心,用小根堆维护,每次小根堆内的数小于可以选取的数,就选;若不能直接选,比较堆顶和当前数即可。
时间复杂度 \(\mathcal O(qnlogn)\),期望得分依旧是 \(40\) 分。
对于这种区间的多次询问,我们很自然的想到了莫队。
但是莫队有一个问题,向后加数当然是容易的,但是如何向前加数以及如何删除成了我们难以解决的问题。
向前加数能够相当于将我们上面的过程倒了过来,用一个大根堆维护没有选取的数,每向前加入一个数,如果可以选择,就选大根堆堆顶。但这样显然是错误的,有可能会出现连续选择首位两个数的情况,对于这种情况,我们很难有效地进行处理。
仔细思考发现,倘若只向前加入一个数,是十分难维护的。我们换一种思维,每次向前加入两个数,同时将它们插入到大根堆,然后直接取堆顶,就避免了这种情况。
至于删除,我们选择逃避,利用回滚莫队即可。
当由于向前操作只能偶数倍的进行,可能会和某些回答要求的范围错开,于是我们需要做两次回滚莫队,每次调节一下左端点的奇偶性。同时由于回滚需要删除,优先队列不支持删除操作,所以需要用到 \(\text{set}\),常数略大。
时间复杂度 \(\mathcal O(n\sqrt n logn)\),期望得分 \(70\) 分。
如果将 \(\text{set}\) 换成压位 \(trie\),时间复杂度将会优化为 \(\mathcal O(n\sqrt n log_w n)\) 或 \(\mathcal O(n\sqrt n loglog n)\),期望得分 \(70\) 到 \(100\)。
我们继续优化上述解法, 考虑分治。
对于区间 \([l,mid]\),用主席树维护每个后缀选取了哪些数,对区间 \([mid + 1,r]\),用主席树维护每个前缀没有选哪些数,接下来考虑将两个区间合并。
合并区间 \([x,mid]\) 和 \([mid + 1, y]\) 时,设区间 \([x,mid]\) 中选了的数变为没有选择的有 \(a\) 个,区间 \([mid + 1, y]\) 中选了的数变为选了的有 \(b\) 个,显然 \(a = b\),即将两个区间内的数状态做了次替换。考虑二分,二分出一个最大的 \(k\) 满足 \([x,mid]\) 中选取的数里面第 \(k\) 小的数小于 \([mid + 1, y]\) 中没有选取的数里面第 \(k\) 大的数。
复杂度 \(\mathcal O((n+q)log^2n)\)
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10, M = 42, INF = 1e15;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
struct Query{ int l, r, id; }que[N];
int n, Q, lim;
int a[N], t[N], ans[N];
int tot, pre[N], suf[N], rt1[N], rt2[N];
int ls[N * M], rs[N * M], siz[N * M], sum[N * M];
vector<Query> vec[N];
inline void update(int &rt, int l, int r, int x, int c)
{
int u = ++tot;
ls[u] = ls[rt], rs[u] = rs[rt];
siz[u] = siz[rt] + c, sum[u] = sum[rt] + t[x] * c, rt = u;
if(l == r) return;
int mid = (l + r) >> 1;
if(x <= mid) update(ls[rt], l, mid, x, c);
else update(rs[rt], mid + 1, r, x, c);
}
inline int Search(int k, int l, int r, int x)
{
if(l == r) return l;
int mid = (l + r) >> 1;
if(x <= siz[ls[k]]) return Search(ls[k], l, mid, x);
else return Search(rs[k], mid + 1, r, x - siz[ls[k]]);
}
inline int ask(int k, int l, int r, int x)
{
if(l == r) return x * t[l];
int mid = (l + r) >> 1;
if(x <= siz[ls[k]]) return ask(ls[k], l, mid, x);
else return ask(rs[k], mid + 1, r, x - siz[ls[k]]) + sum[ls[k]];
}
inline int Sol(int l, int r) //反悔贪心
{
int res = 0;
priority_queue<int, vector<int>, greater<int> > q;
for(register int i = l; i <= r; i++){
if((int)q.size() < (i - l + 2) / 2) res += t[a[i]], q.push(t[a[i]]);
else if(q.top() < t[a[i]]) res -= q.top(), res += t[a[i]], q.pop(), q.push(t[a[i]]);
}
return res;
}
inline void Binary(int l, int r, vector<Query> &Qy)
{
if(Qy.empty()) return;
int mid = (l + r) >> 1;
vector<Query> Ls, Rs; //左右儿子
for(register int i = l; i <= r; i++) vec[i].clear();
for(register Query it : Qy){
if(it.r <= mid) Ls.push_back(it);
else if(it.l > mid) Rs.push_back(it);
else vec[it.l].push_back(it);
}
for(register int opt = 0; opt < 2; opt++){ //调节奇偶性
int p = mid + opt;
rt1[p] = rt2[p + 1] = 0, pre[p] = suf[p + 1] = 0, tot = 0;
int L = INF, R = 0;
for(register int i = p - 1; i >= l; i -= 2)
for(register Query it : vec[i]) L = it.l, R = max(R, it.r);
priority_queue<int> ded;
priority_queue<int, vector<int>, greater<int>> hav;
for(register int i = p + 1; i <= R; i++){
rt1[i] = rt1[i - 1], pre[i] = pre[i - 1];
if(!((i - p) % 2)){ //偶数形扩展,一次扩展两个
int x = i, y = i - 1;
if(a[x] < a[y]) swap(x, y);
pre[i] += t[a[x]], hav.push(a[x]);
if(!hav.empty() && a[y] > hav.top())
pre[i] -= t[hav.top()], pre[i] += t[a[y]], update(rt1[i], 1, lim, hav.top(), 1), hav.pop(), hav.push(a[y]);
else update(rt1[i], 1, lim, a[y], 1); //维护一个没选的前缀
}
}
for(register int i = p; i >= L; i--){ //维护一个选了的后缀
rt2[i] = rt2[i + 1], suf[i] = suf[i + 1], ded.push(a[i]);
if(!((p - i + 1) % 2)) update(rt2[i], 1, lim, ded.top(), 1), suf[i] += t[ded.top()], ded.pop();
}
for(register int i = p - 1; i >= L; i -= 2){
for(register Query it : vec[i]){
int x = 0, y = min((p - i + 1) / 2, (it.r - p) / 2), tem = suf[i] + pre[it.r];
while(x < y){
int amo = (x + y + 1) >> 1;
if(Search(rt1[it.r], 1, lim, (it.r - p) / 2 - amo + 1) >= Search(rt2[i], 1, lim, amo)) x = amo;
else y = amo - 1;
}
if(x){
tem += sum[rt1[it.r]] - ask(rt2[i], 1, lim, x);
if(x != (it.r - p) / 2) tem -= ask(rt1[it.r], 1, lim, (it.r - p) / 2 - x);
}
ans[it.id] += tem;
}
}
}
Binary(l, mid, Ls), Binary(mid + 1, r, Rs);
}
signed main()
{
n = read(), Q = read();
for(register int i = 1; i <= n; i++) a[i] = read(), t[i] = a[i];
sort(t + 1, t + n + 1), lim = unique(t + 1, t + n + 1) - t - 1;
for(register int i = 1; i <= n; i++) a[i] = lower_bound(t + 1, t + lim + 1, a[i]) - t;
vector<Query> Qy;
for(register int i = 1, l, r; i <= Q; i++){
l = read(), r = read();
if((r - l + 1) & 1) ans[i] += t[a[r]], r--;
if(l > r) continue;
if(r - l + 1 <= 20) ans[i] += Sol(l, r);
else Qy.push_back((Query){l, r, i});
}
Binary(1, n, Qy);
for(register int i = 1; i <= Q; i++) printf("%lld\n", ans[i]);
return 0;
}