JOISC2017
A.Cultivation
首先刮风顺序无关,考虑枚举横风总次数。会发现我们可以先把所有横风视为東风,然后把一些改成南风就相当于把整个坐标构成的框往右移。容易发现在刮完横风之后所有草的位置都是一条横线。然后考虑南北风的计算。发现钦定了横风次数和坐标网格位移之后,每一列的最靠上的有草的格子中最靠下的,会对南风次数的下界提出要求。每一列的最靠下的有草的格子中最靠上的,会对北风次数的下界提出要求。而每一列中每一段连续的没有草的格子,会对南北风次数和提出要求。将三个要求结合起来容易求出南北风最小总次数。注意到坐标网格的位移并不会影响任何线段的位置关系,因此可以考虑求出所有列的三个东西分别是多少,然后做一遍单调队列求答案。由于本质不同的列是 \(\mathcal{O}(n)\) 级别,而本质不同的横风次数是 \(\mathcal{O}(n^2)\) 级别,因此这样做的总复杂度是 \(\mathcal{O}(n^3)\)。接下来我们需要求出所有本质不同的列及其答案。观察发现每段本质相同的列的结尾要么是一个线段的右端点,要么是一个线段的左端点再往左一格。而这种东西个数线性,随着枚举的横风次数的增加每个的答案只会变化 \(\mathcal{O}(n)\) 次。那么这样可以维护出每个横风次数对应的总答案。接下来我们要找出这本质不同的平方种横风次数。一些平凡的是导致那些关键列的答案变化的横风次数,但是这些不够。
首先确定我们只在每个线段的右端点和左端点的左一格计算答案。那么当横风次数的增加导致一个右端点往左作出的网格的边界被其他线段碰到的时候,我们需要重算一遍答案。当横风次数的增加导致一个左端点的左一格往右作出的网格边界碰到了一条线段,我们也要重算一遍答案。除此之外,还有每个点原本到左边界、右边界的距离,以及这两个距离的和(\(m - 1\))。我们只要在这些横风次数的位置各统计一遍答案,即可保证统计到最优解。随着横风次数的增加动态维护每个关键列的答案(每次 \(\mathcal{O}(n)\) 更新),每次统计答案使用单调队列,总复杂度 \(\mathcal{O}(n^3)\)。
代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <array>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, m, K;
array<int, 2> p[305], p2[305];
vector<array<int, 3> > vec;
// eq ans last point, 0 : left of left, 1 : right
array<int, 3> ans[305][2]; // up, down, between
int Lmn;
array<int, 3> calc(int len, int y) {
array<int, 3> ret{ 0, 0, 0 }; int lst = 0;
for (int i = 1; i <= K; i++) {
if (p2[i][1] - len <= y && y <= p2[i][1]) {
if (lst) ret[1] = max(ret[1], p2[i][0] - lst - 1);
else ret[0] += p2[i][0] - 1;
lst = p2[i][0];
}
}
ret[2] += (n - lst);
return ret;
}
struct Mono_Q {
pair<int, int> q[100005]; int ql, qr;
void init() { ql = 0, qr = -1; }
void push(int t, int v) { while (ql <= qr && q[qr].second <= v) --qr; q[++qr] = make_pair(t, v); }
void pop(int t) { while (ql <= qr && q[ql].first <= t) ++ql; }
int query() { return q[ql].second; }
} q[3];
pair<int, array<int, 3> > s1[305], s2[305], s[605];
int _brk[1205], brk[1205], c1, c2;
int work(int len) {
for (int i = 1; i <= K; i++) s1[i] = make_pair(p[i][1], ans[i][1]), s2[i] = make_pair(p[i][1] - len - 1, ans[i][0]);
merge(s1 + 1, s1 + K + 1, s2 + 1, s2 + K + 1, s + 1); c1 = unique(s + 1, s + K * 2 + 1) - s - 1;
for (int i = 1; i <= c1; i++) _brk[i] = s[i].first, _brk[i + c1] = min(p[K][1], s[i].first + m - 1);
merge(_brk + 1, _brk + c1 + 1, _brk + c1 + 1, _brk + 2 * c1 + 1, brk + 1);
c2 = unique(brk + 1, brk + c1 * 2 + 1) - brk - 1;
q[0].init(), q[1].init(), q[2].init();
int ret = inf;
s[0].first = -inf;
for (int i = 1, j = 1; i <= c2; i++) {
while (j <= c1 && s[j - 1].first < brk[i]) q[0].push(s[j].first, s[j].second[0]), q[1].push(s[j].first, s[j].second[1]), q[2].push(s[j].first, s[j].second[2]), ++j;
q[0].pop(brk[i] - m), q[1].pop(brk[i] - m), q[2].pop(brk[i] - m);
if (brk[i] >= p[1][1] - len - 1 + m) ret = min(ret, max(q[0].query() + q[2].query(), q[1].query()));
}
return ret + len;
}
signed main() {
cin >> n >> m >> K;
for (int i = 1; i <= K; i++) cin >> p[i][0] >> p[i][1], p2[i] = p[i];
sort(p + 1, p + K + 1, [](array<int, 2> x, array<int, 2> y) { return x[1] < y[1]; });
sort(p2 + 1, p2 + K + 1);
for (int i = 1; i <= K; i++) {
vec.push_back({ m - p[i][1], -1, 0 });
vec.push_back({ p[i][1] - 1, -1, 0 });
for (int j = 1; j <= K; j++) {
if (p[i][1] >= p[j][1]) continue;
// i left j right
vec.push_back({ p[j][1] - p[i][1] - 1, j, 0 });
vec.push_back({ p[j][1] - p[i][1], i, 1 });
vec.push_back({ p[j][1] - (p[i][1] - m + 1), -1, 0 });
vec.push_back({ p[i][1] - (p[j][1] - m + 1), -1, 0 });
}
}
vec.push_back({ m - 1, -1, 0 });
for (int i = 1; i <= K; i++) ans[i][0] = calc(0, p[i][1] - 1), ans[i][1] = calc(0, p[i][1]);
for (int i = 1; i < K; i++) Lmn = max(Lmn, p[i + 1][1] - p[i][1] - 1);
Lmn = max(Lmn, p[1][1] - 1 + m - p[K][1]);
sort(vec.begin(), vec.end());
auto f = [&](int &x, int &y, int &z, array<int, 3> w) { x = w[0], y = w[1], z = w[2]; };
int x, y, z, k = 0; while (k < (int)vec.size() && vec[k][0] <= Lmn) {
f(x, y, z, vec[k]);
if (y != -1) ans[y][z] = calc(x, p[y][1] - (!z) * (x + 1)); ++k;
}
if (k == (int)vec.size()) return cout << work(Lmn) << "\n", 0;
int Ans = work(Lmn);
for (int j; k < (int)vec.size(); k = j) {
j = k;
while (j < (int)vec.size() && vec[j][0] == vec[k][0]) {
f(x, y, z, vec[j]);
if (y != -1) ans[y][z] = calc(x, p[y][1] - (!z) * (x + 1)); ++j;
}
Ans = min(Ans, work(vec[k][0]));
}
cout << Ans << "\n";
return 0;
}
B.Port Facility
将区间按相交关系建边,若有解,容易发现答案为 \(2^{连通块个数}\)。只需要判断是否有解。
我们考虑直接 bfs。过程中使用数据结构快速查找下一层节点。每次相当于需要找到左端点位于 \([l, r]\),右端点 \(> r\) 的所有剩余区间。我们线段树维护左端点,每个结点上维护一个单调的右端点集合。如果按升序加入右端点的话就不需要开 set 之类的东西了。然后每次找出边的时候直接在线段树上查就可以了。这样我们可以完成一遍染色,染完色再判一遍每个色内部是否合法即可。
线段树递归真慢。但还有我不会的并查集做法。
代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#define lowbit(x) ((x) & (-(x)))
using namespace std;
const int P = 1000000007, N = 2000002;
namespace Fread {
#define getchar() p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++
char buf[1<<21], *p1, *p2, ch;
long long read() {
long long ret = 0, neg = 0; char c = getchar(); neg = (c == '-');
while (c < '0' || c > '9') c = getchar(), neg |= (c == '-');
while (c >= '0' && c <= '9') ret = ret * 10 + c - '0', c = getchar();
return ret * (neg ? -1 : 1);
}
}
using Fread::read;
int n;
bool usd[4000005];
int nxt[1000005], tmp;
struct Segment_Tree {
int tl[4000005], cur[4000005], lf[1000005];
int val[21000005];
void asdf(int o, int l, int r) {
if (l == r)
return lf[l] = o, void();
int mid = (l + r) >> 1;
asdf(o << 1, l, mid);
asdf(o << 1 | 1, mid + 1, r);
}
void add(int x) {
int o = lf[x];
while (o) ++cur[o], o >>= 1;
}
void Build() {
for (int o = 1; o <= n * 4; o++) tl[o] = tl[o - 1] + cur[o - 1];
for (int o = 1; o <= n * 4; o++) cur[o] = tl[o] - 1;
}
void Add(int x, int y) {
int o = lf[x];
while (o) val[++cur[o]] = y, o >>= 1;
}
void Query(int o, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) {
while (cur[o] != tl[o] - 1) {
if (val[cur[o]] <= v)
break;
if (!usd[N + val[cur[o]]])
nxt[++tmp] = val[cur[o]];
--cur[o];
}
return;
}
int mid = (l + r) >> 1;
if (L <= mid)
Query(o << 1, l, mid, L, R, v);
if (R > mid)
Query(o << 1 | 1, mid + 1, r, L, R, v);
}
} seg1, seg2;
int l[1000005], r[1000005], bel[2000005];
int p1[2000005], p2[2000005];
int ans = 1, asdfasdf;
bool vis[1000005];
int clr[1000005];
queue<int> q;
void bfs(int S) {
q.push(S);
usd[N - l[S]] = 1, usd[N + r[S]] = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 1;
tmp = 0;
if (p1[l[x]] < p1[r[x]])
seg1.Query(1, 1, n, p1[l[x]] + 1, p1[r[x]], r[x]);
asdfasdf = tmp;
for (int i = 1, v; i <= tmp; i++) v = nxt[i] = bel[nxt[i]], usd[N - l[v]] = 1, usd[N + r[v]] = 1;
if (p2[l[x]] + 1 <= p2[r[x]] - 1)
seg2.Query(1, 1, n, p2[l[x]] + 1, p2[r[x]] - 1, -l[x]);
for (int i = asdfasdf + 1, v; i <= tmp; i++) v = nxt[i] = bel[-nxt[i]], usd[N - l[v]] = 1, usd[N + r[v]] = 1;
for (int i = 1; i <= tmp; i++) q.push(nxt[i]), clr[nxt[i]] = clr[x] ^ 1;
}
}
int stk[2][1000005], sz[2];
bool chk() {
for (int i = 1; i <= n * 2; i++) {
int t = clr[bel[i]];
if (r[bel[i]] == i && stk[t][sz[t]] != bel[i])
return 0;
else if (r[bel[i]] == i)
--sz[t];
else
stk[t][++sz[t]] = bel[i];
}
return 1;
}
int main() {
n = read();
for (int i = 1; i <= n; i++) l[i] = read(), r[i] = read(), bel[l[i]] = bel[r[i]] = i;
seg1.asdf(1, 1, n), seg2.asdf(1, 1, n);
for (int i = 1; i <= n * 2; i++) {
p1[i] = p1[i - 1] + (l[bel[i]] == i);
p2[i] = p2[i - 1] + (r[bel[i]] == i);
}
for (int i = n * 2; i; i--) {
if (l[bel[i]] == i)
seg2.add(p2[r[bel[i]]]);
}
for (int i = 1; i <= n * 2; i++) {
if (r[bel[i]] == i)
seg1.add(p1[l[bel[i]]]);
}
seg1.Build(), seg2.Build();
for (int i = n * 2; i; i--) {
if (l[bel[i]] == i)
seg2.Add(p2[r[bel[i]]], -l[bel[i]]);
}
for (int i = 1; i <= n * 2; i++) {
if (r[bel[i]] == i)
seg1.Add(p1[l[bel[i]]], r[bel[i]]);
}
for (int i = 1; i <= n; i++) {
if (!vis[i])
ans = ans * 2 % P, bfs(i);
}
cout << (chk() ? ans : 0) << "\n";
return 0;
}
C.Sparklers
首先重要结论是每个时刻火种只会在一个人手上,并且一定在火种熄灭的时候传递,这样可以保证总燃烧时间最长,也就是总路程最长。然后每个时刻一定是中间一段区间的人集中在一个点上,而其中一个人持有火种。
我们先二分答案 \(v\)。那么假设当前区间 \([l, r]\) 已经合并成一个点,接下来假设我们要往左扩展一个人(往右同理)。我们需要保证中间这段区间的总燃烧时间能够支付把 \([l - 1, r]\) 全合到一起的时间。也就是 \(T(r - l + 1) \ge \frac{x_r - x_{l - 1}}{2v}\),即 \(2vTr - x_r \ge 2vT(l - 1) - x_{l - 1}\)。若设 \(a_i = 2vT(i - 1) - x_i\),则条件即为 \(a_r \ge a_{l - 1}\),也就是在区间扩展途中我们需要保证右端点时刻大于等于左端点的值,然后问你能不能从 \([K, K]\) 扩展到 \([1, n]\)。如果贪心扩展,我们会发现两个特殊位置:\(g_l\) 为 \([1, K]\) 的 \(\min a\),\(g_r\) 为 \([K, n]\) 的 \(\max a\)。扩展到这两个位置之后一定就走不动了。这个时候考虑倒着走。也就是说我们只需要从 \([K, K]\) 走到 \([g_l, g_r]\),再从 \([1, n]\) 走到 \([g_l, g_r]\),如果两边都能走到,那么就是合法的。
接下来我们贪心扩展,每次扩展一边边界时不断扩展,直到扩展到一个比原来位置更容易满足限制的位置或不满足另一个端点的限制了为止。接下来考虑这个停止的位置,如果它确实比原来更容易满足限制,则我们更新这个端点的位置。如果某个时刻我们还没有到达目标,但是往左和往右都无法进行这样的合法扩展,那么就认为不合法,返回即可。
代码
#include <iostream>
#define int long long
using namespace std;
int n, K, t;
__int128 a[100005];
int X[100005];
bool chk(int v) {
for (int i = 1; i <= n; i++) a[i] = X[i] - ((__int128)2 * v) * i * t;
int gl = K, gr = K;
for (int i = K; i <= n; i++) {
if (a[i] <= a[gr])
gr = i;
}
for (int i = K; i; i--) {
if (a[i] >= a[gl])
gl = i;
}
int l = K, r = K;
for (; ; ) {
int tl = l, tr = r, x = 0;
while (1) {
if (tl > gl && a[tl - 1] >= a[r])
--tl;
else
break;
if (a[tl] >= a[l] || tl == gl)
break;
}
if (a[tl] >= a[l] && tl != l)
l = tl, x = 1;
while (1) {
if (tr < gr && a[l] >= a[tr + 1])
++tr;
else
break;
if (a[tr] <= a[r] || tr == gr)
break;
}
if (a[tr] <= a[r] && tr != r)
r = tr, x = 1;
if (!x)
break;
}
if (l != gl || r != gr)
return 0;
for (l = 1, r = n; ; ) {
int tl = l, tr = r, x = 0;
while (1) {
if (tl < gl && a[tl] >= a[r])
++tl;
else
break;
if (a[tl] >= a[l] || tl == gl)
break;
}
if (a[tl] >= a[l] && tl != l)
l = tl, x = 1;
while (1) {
if (tr > gr && a[l] >= a[tr])
--tr;
else
break;
if (a[tr] <= a[r] || tr == gr)
break;
}
if (a[tr] <= a[r] && tr != r)
r = tr, x = 1;
if (!x)
break;
}
if (l != gl || r != gr)
return 0;
return 1;
}
signed main() {
cin >> n >> K >> t;
for (int i = 1; i <= n; i++) cin >> X[i];
int l = 0, r = 1000000000000, mid, ans;
while (l <= r) {
mid = (l + r) >> 1;
if (chk(mid))
ans = mid, r = mid - 1;
else
l = mid + 1;
}
cout << ans << "\n";
return 0;
}
D.Arranging Tickets
首先先把所有要求都变成区间的形式,然后考虑把一些要求翻转成前后缀。二分答案之后,我们考虑要翻转哪些东西。
首先注意到任意两个翻转的区间不会不交。因如果有两个翻转的区间不交,那么我们同时取消这两个区间的翻转,没有位置的覆盖次数会增加,只会有减少。因此这样不优。
于是所有被翻转的区间会存在一个交点 \(p\)。根据感性理解这个位置肯定是原序列的最大值,否则看起来就很不优。然后考虑这个位置最后变成了什么。又会发现所有翻转做完之后 \(p\) 位置的值一定是全局最大值或最大值减一。否则你取消提供当前所有区间交的右端点的那个区间的翻转,并考虑右边的全局最大值(如果它在右边的话)。这个时候这个值减少了 \(1\),而 \(p\) 位置的值增加了 \(1\)。如果这次取消翻转给 \(p\) 左边的全局最大值(如果左边也有的话)也增加了 \(1\),那我们再取消提供当前所有区间交的左端点的那个区间的翻转,会发现它相对原来的值没有改变,而 \(p\) 位置的值相对原来总共增加了 \(2\)。综上我们总是可以通过操作在不改变最大值的情况下让 \(p\) 位置变成最大值或其减一。而最终最大值肯定是当前二分的答案。于是最终 \(p\) 位置的值只会有两个。
有了这个结论我们可以证明 \(p\) 位置必然是原序列最大值。这也就是说所有被翻转区间的交不包含原序列的最大值位置(否则可以直接把最大值位置视为 \(p\))。那么由于 \(p\) 不是原序列 \(a\) 最大值(设为 \(a_q = mx\)),则 \(a_p < mx\)。设所有被翻转区间的交为 \(S\),则由于 \(p \in S, q \notin S\),存在至少一次操作翻转了包含 \(p\) 且不包含 \(q\) 的区间。而这种操作只会让 \(a_q - a_p\) 从 \(1\) 变大,而不会让它们的差缩小到 \(\le 1\)。又其他操作都不会改变 \(a_p\) 和 \(a_q\) 的相对大小关系,则我们推出这种情况与上面的结论矛盾,因此假设不成立,\(p\) 必为原序列最大值位置。
那么现在我们已经确定所有区间的交之一 \(p\) 及其最终要变成什么了,我们可以求出 \(p\) 最终要被修改几次,进而可以求出 \(p\) 前面的所有位置需要多少左端点更小的区间被翻转。设每个位置的这个值为 \(v_i(i < p)\),二分答案的值为 \(x\),\(p\) 最终要变成 \(y\),则我们有 \(a_i - v_i + a_p - y - v_i \le x\),也就是 \(v_i \ge \frac{a_i + a_p - x - y}{2}\)。从左往右考虑每个点 \(i\),每次加入包含 \([i, p]\) 的区间,在所有已经加入的区间中贪心选出 \(v_i\) 个右端点最大的翻转,最后检查右边所有位置的最大值是否超过限制。若中途出现选不够 \(v_i\) 或最终右边最大值超过限制的情况,则可以证明当前答案不合法。然后随便拿个 set 或 pq 维护右端点,就做完了。
代码
#include <iostream>
#include <string.h>
#include <vector>
#include <queue>
#define int long long
using namespace std;
int n, m;
int A[200005], B[200005], C[200005];
priority_queue<pair<int, int> > q;
int a[200005], p;
vector<pair<int, int> > vec[200005];
int tmp[200005];
bool chk(int x, int t) {
if (t <= 0)
return 1;
while (q.size()) q.pop();
memset(tmp, 0, sizeof tmp);
for (int i = 1, cur = 0; i <= p; i++) {
for (auto v : vec[i]) {
if (v.first >= p)
q.push(v);
}
int y = (a[i] + t - x + 1) / 2, req = y - cur;
while (req > 0 && q.size()) {
pair<int, int> p = q.top();
q.pop();
int v = min(p.second, req);
p.second -= v, req -= v;
if (p.second)
q.push(p);
tmp[::p + 1] -= v, tmp[p.first + 1] += v * 2;
}
cur += max(0ll, y - cur);
if (req > 0)
return 0;
}
bool ret = 1;
for (int i = p + 1; i <= n; i++) ret &= (a[i] + (tmp[i] += tmp[i - 1]) <= x);
return ret;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> A[i] >> B[i] >> C[i];
if (A[i] > B[i])
swap(A[i], B[i]);
a[A[i]] += C[i], a[B[i]] -= C[i];
vec[A[i]].emplace_back(B[i] - 1, C[i]);
}
for (int i = 1; i <= n; i++) a[i] += a[i - 1];
for (int i = p = 1; i <= n; i++) (a[i] >= a[p]) ? (p = i) : 0;
int l = 1, r = 100000000000000, mid, ans;
while (l <= r) {
mid = (l + r) >> 1;
if (chk(mid, a[p] - mid) | chk(mid, a[p] - mid + 1))
ans = mid, r = mid - 1;
else
l = mid + 1;
}
cout << ans << "\n";
return 0;
}
E.Broken Device
考虑每三位分一组。如果一组都是好的,我们让他至少传递 \(2 \text{bit}\) 的信息;如果只坏了一个,我们让它至少传递 \(1 \text{bit}\) 的信息。否则我们开摆。那么接下来就是根据每种坏的位置构造映射表。但是构造的时候会遇到问题:东西不够用。理论上我们需要四个不同的编码来保证只坏一个的组能够传递至少一个信息,但是实际上编码不够用。这时候,我们考虑这一位的下一位,只要在这个组的坏法下 这一位 + 下一位 的两种组合都能表示,那么就可以解决这个问题了。那么这样就容易构造了。我构造的放在代码里了。
代码
// #include "Annalib.h"
// #include "Brunolib.h"
#include "Broken_device_lib.h"
#include <iostream>
#include <string.h>
#include <vector>
using namespace std;
int bit[1065];
int bad[1005];
void Anna( int N, long long X, int K, int P[] ) {
memset(bad, 0, sizeof bad);
for (int i= 0 ; i < K; i++) bad[P[i]] = 1;
for (int i= 1060; i > 1000; i--) bit[i] = ((X >> (i - 1001)) & 1);
int cur = 1060;
for (int i = 0; i < 150; i += 3) {
int j = i + 1, k = i + 2;
if (bad[i] + bad[j] + bad[k] >= 2) {
Set(i, 0), Set(j, 0), Set(k, 0);
continue;
}
if (bad[i] + bad[j] + bad[k] == 0) {
int x = bit[cur--], y = bit[cur--];
if (!x && !y) Set(i, 0), Set(j, 1), Set(k, 1);
else if (!x && y) Set(i, 1), Set(j, 1), Set(k, 1);
else if (x && !y) Set(i, 1), Set(j, 1), Set(k, 0);
else Set(i, 1), Set(j, 0), Set(k, 0);
} else {
int x = bit[cur--], y;
if (bad[i]) x ? (Set(i, 0), Set(j, 0), Set(k, 1)) : (Set(i, 0), Set(j, 1), Set(k, 0));
else if (bad[j]) x ? (Set(i, 0), Set(j, 0), Set(k, 1)) : (Set(i, 1), Set(j, 0), Set(k, 1));
else {
if (!x) Set(i, 0), Set(j, 1), Set(k, 0);
else bit[cur--] ? (Set(i, 1), Set(j, 0), Set(k, 0)) : (Set(i, 1), Set(j, 1), Set(k, 0));
}
}
}
}
string cor[8] = { "", "1", "0", "00", "11", "0", "10", "01" };
long long Bruno( int N, int A[] ) {
string str;
for (int i = 0; i < N; i += 3) {
int val = (A[i] << 2) + (A[i + 1] << 1) + A[i + 2];
str += cor[val];
}
long long ret = 0;
for (int i= 0; i < 60; i++) ret = ret << 1 | (str[i] - '0');
return ret;
}
F.Railway Trip
考虑对每个点求出其跳 \(i\) 步能够跳到的区间,容易发现所有点的所有这些区间要么不交要么包含,因此可以建出一棵树来。则我们要求的就是从两个点往上跳,一共要跳几次才能让两个点所在的区间相邻。于是倍增一下求出每个点跳 \(2^i\) 步到达的区间,每次询问倍增跳祖先即可。
代码
G.Long Distance Coach
模拟赛某题的原题。这下这下了。
容易发现一个人如果下车,那一定是越早赶走越好。可以发现每个饮水点能够干掉其前面的若干个人。我们直接对着这个东西 dp,\(f_i\) 表示前 \(i\) 个人考虑完了的最小代价。不赶走这个人的转移是平凡的,赶走这个人就要求这个人到下一个人中间有饮水点,然后考虑我们用这个饮水点要往前干掉哪些人。列出方程发现可以斜率优化,于是就做完了。
代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <array>
#define int long long
using namespace std;
int X, n, m, W, T;
array<int, 2> p[200005];
int f[200005];
struct Line {
int k, b;
Line(int x = 0, int y = 0) { k = x, b = y; }
int operator()(int x) { return k * x + b; }
};
inline bool chkd(Line a, Line b, Line c) { return (a.k - b.k) * (c.b - b.b) <= (b.k - c.k) * (b.b - a.b); }
struct Cv {
Line stk[200005];
int sz;
void push(Line l) {
while (sz > 1 && chkd(stk[sz - 1], stk[sz], l)) --sz;
stk[++sz] = l;
}
int query(int x) {
int l = 1, r = sz - 1, mid, ans = sz;
while (l <= r) {
mid = (l + r) >> 1;
if (stk[mid](x) < stk[mid + 1](x))
ans = mid, r = mid - 1;
else
l = mid + 1;
}
return stk[ans](x);
}
} c;
vector<int> vec[200005];
array<int, 2> s[200005];
signed main() {
cin >> X >> n >> m >> W >> T;
for (int i = 1, t; i <= n; i++) {
cin >> t;
s[i][0] = t % T, s[i][1] = W * (t / T);
}
++n, s[n][0] = X % T, s[n][1] = W * (X / T);
for (int i = 1; i <= m; i++) cin >> p[i][0] >> p[i][1];
sort(s + 1, s + n + 1);
sort(p + 1, p + m + 1);
for (int i = 1; i <= m; i++) p[i][1] += p[i - 1][1];
f[0] = W * (X / T + 1); c.push(Line(0, f[0]));
int j = 1; for (; j <= n && s[j][0] <= p[1][0]; j++);
for (int i = 1; i <= m; i++) {
f[i] = f[i - 1] + W * (X / T + (p[i][0] < X % T));
while (j <= n && (i == m || s[j][0] <= p[i + 1][0])) f[i] = min(f[i], c.query(s[j][1]) + p[i][1] + s[j][1] * i), ++j;
c.push(Line(-i, -p[i][1] + f[i]));
}
cout << f[m] << "\n";
return 0;
}
H.Long Mansion
假设目标在出发点左边。
考虑直接从出发点往目标点走,一直走不回头。那这个时候发现如果遇到了没有钥匙的门,我们就完蛋了。这个时候只好回去查看另一个方向,我们需要在另一个方向找到我们需要的钥匙。然后再一直往目标走,如此循环。考虑我们往左遇到门 \(x\) 走不动时往右走,需要右边符合怎样的条件。首先我们需要走到 \(x\) 右边的第一个它的钥匙 \(p\),这个过程中我们要保证所有遇到的门都能开。也就是从起点到 \(p\) 的所有这些门左边的第一个它的钥匙必须在 \(x\) 的右边。容易发现对于一个固定的 \(x\) 来说,起点只相当于一个限制,随着起点的往左,我们要求左边第一个钥匙在 \(x\) 右边的门在变多,也就越来越难满足。因此可以考虑对每个 \(x\) 求出一个最小可以满足的起点 \(g_x\),使得这右边的起点都可以经过 \(x\),而左边的都不能。如果求出这个,则每次询问只需要检查区间 \(\max\) 和起点的关系即可。而 \(g\) 也很好求,对每个门都二分一下,每次查询区间 \(\min\) 即可。而目标在出发点右的情况就是对称的。于是我们就做完了。
代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int n, q;
int c[500005];
vector<int> hv[500005];
struct ST {
int mx[20][500005], t;
void Build(int *a, int m, int tp) {
t = tp;
for (int i= 1;i <= m; i++) mx[0][i] = t * a[i];
for (int i= 1; (1 << i) <= m; i++) {
for (int j = 1; j + (1 << i) - 1 <= m; j++)
mx[i][j] = max(mx[i - 1][j], mx[i - 1][j + (1 << (i - 1))]);
}
}
int Query(int l, int r) {
int k = 31 - __builtin_clz(r - l + 1);
return t * max(mx[k][l], mx[k][r - (1 << k) + 1]);
}
} sfl, sfr, sgl, sgr;
int fl[500005], fr[500005], gl[500005], gr[500005];
int ap[500005];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i < n; i++) cin >> c[i];
for (int i = 1, k, x; i <= n; i++) {
cin >> k;
while (k--) cin >> x, hv[i].emplace_back(x);
}
for (int i = 1; i < n; i++) {
for (auto v : hv[i]) ap[v] = i;
fl[i] = ap[c[i]];
}
for (int i = 1; i <= n; i++) ap[i] = n + 1;
for (int i = n - 1; i; i--) {
for (auto v : hv[i + 1]) ap[v] = i + 1;
fr[i] = ap[c[i]];
}
sfl.Build(fl, n, -1), sfr.Build(fr, n, 1);
for (int i = 1; i < n; i++) {
int l = i + 1, r = fr[i] - 1, mid; gr[i] = fr[i];
if (fr[i] != n + 1) {
while (l <= r) {
mid = (l + r) >> 1;
if (sfl.Query(mid, fr[i] - 1) > i)
gr[i] = mid, r = mid - 1;
else
l = mid + 1;
}
} else fr[i] = n + 1;
l = fl[i] + 1, r = i, gl[i] = fl[i];
if (fl[i]) {
while (l <= r) {
mid = (l + r) >> 1;
if (sfr.Query(fl[i], mid - 1) <= i)
gl[i] = mid, l = mid + 1;
else
r = mid - 1;
}
} else gl[i] = 0;
}
sgl.Build(gl, n, -1), sgr.Build(gr, n, 1);
cin >> q;
while (q--) {
int x, y;
cin >> x >> y;
if (y < x)
cout << (sgr.Query(y, x - 1) <= x ? "YES\n" : "NO\n");
else
cout << (sgl.Query(x, y - 1) >= x ? "YES\n" : "NO\n");
}
return 0;
}
I.Natural Park
考虑维护一个与 \(0\) 连通的已知点集 \(S\)(点集内的边也已知)。每次尝试往点集中加入新点。具体地,我们随机挑选出一个未知点 \(x\),检查其是否与 \(S\) 连通。若连通,则求出其与 \(S\) 中点的连边,再将其标记为已知点。若不连通,我们尝试找到一条其走到 \(S\) 的路径。考虑按顺序不断允许更多点可走的情况下,一定存在一个点使得允许它可走时 \(x\) 第一次与 \(S\) 连通。我们可以二分找到这个点。每次把未知点的一个前缀和 \(S, x\) 并起来询问 \(x\) 能否到达 \(S\)。这样我们就找到了 \(x\) 到 \(S\) 的某条路径上的某个点 \(y\)。若 \(y\) 与 \(S\) 连通了,则变成刚才 \(x\) 直接就和 \(S\) 连通的情况,将 \(y\) 加入 \(S\) 后继续根据 \(x\) 与 \(S\) 的连通性讨论。否则还是相当于 \(x\) 与 \(S\) 不连通的情况,我们同样方法找到 \(y\) 到 \(S\) 的某条路径上的某个点,递归做下去。具体实现上可以只开一个栈,每次取出栈顶,若不连通则再往栈里加元素,否则将栈顶加入 \(S\) 并弹出。
接下来我们需要在将一个点 \(x\) 加入 \(S\) 的时候求出其与 \(S\) 中所有点的连边情况。我们考虑为连通块钦定一个根 \(r\),求出从 \(r\) 开始的一个拓扑序,然后在拓扑序上二分,每次加入拓扑序的一个前缀并询问 \(x\) 是否与根连通。通过这种方式我们可以求出 \(S\) 中一个与 \(x\) 有边相连的点。接下来删去这个点,若原点集此时还是一个连通块,则我们继续做。否则其分裂成若干个连通块,我们对每个连通块分别做即可。
代码
#include "park.h"
#include <iostream>
#include <algorithm>
#include <string.h>
#include <random>
#include <vector>
using namespace std;
random_device rd;
mt19937 mtrand(rd());
int n;
vector<int> G[1405];
int kwn[1405];
int stk[1405], sz;
vector<int> uk, U;
int q_place[1405];
bool Con(int x, int y, vector<int> &vec, int l, int r, int *bas = kwn) {
for (int i = 0; i < n; i++) q_place[i] = bas[i];
for (int i = l; i <= r; i++) q_place[vec[i]] = 1;
q_place[x] = q_place[y] = 1;
return Ask(min(x, y), max(x, y), q_place);
}
int F(vector<int> &vec, int x, int *targ = kwn) {
int y = 0;
for (int i = 0; i < n; i++) if (targ[i] == 1) { y = i; break; }
int l = 0, r = (int)vec.size() - 1, mid, ret = -1;
while (l <= r) {
mid = (l + r) >> 1;
if (Con(x, y, vec, 0, mid, targ)) ret = mid, r = mid - 1;
else l = mid + 1;
}
return ret == -1 ? -1 : vec[ret];
}
bool mark[1405];
vector<int> vec;
int vis[1405];
bool vis2[1405][1405];
void dfs(int x) {
vis[x] = 1, vec.emplace_back(x);
for (int v : G[x]) if (kwn[v] && !mark[v] && !vis[v]) dfs(v);
}
void dfs2(int x, bool* visr) { visr[x] = 1; for (int v : G[x]) if (kwn[v] && !mark[v] && !visr[v]) dfs2(v, visr); }
void Findedge(int x, int r) {
memset(vis2[x], 0, sizeof vis2[x]);
vec.clear();
dfs(r); memset(vis, 0, sizeof vis); vis[r] = 1;
int y = F(vec, x, vis); vis[r] = 0;
if (y == -1) return;
mark[y] = 1;
for (int v : G[y]) {
if (!mark[v] && kwn[v] && !vis2[r][v]) {
dfs2(v, vis2[x]);
Findedge(x, v);
}
}
Answer(min(x, y), max(x, y));
G[y].emplace_back(x), G[x].emplace_back(y);
}
void Detect(int T, int N) {
n = N;
kwn[sz = 0] = 1;
for (int i = 1; i < N; i++) uk.emplace_back(i), U.emplace_back(i);
while (uk.size()) {
shuffle(uk.begin(), uk.end(), mtrand);
stk[sz = 1] = uk[0];
while (sz) {
if (Con(0, stk[sz], uk, 0, -1)) {
memset(mark, 0, sizeof mark);
Findedge(stk[sz], 0);
kwn[stk[sz]] = 1;
uk.erase(find(uk.begin(), uk.end(), stk[sz]));
--sz;
} else stk[sz + 1] = F(U, stk[sz]), ++sz;
}
}
}
J.Abduction 2
直接记忆化搜索 \(f_{i, j, k}\) 表示当前在 \((i, j)\) 交点,往 \(k\) 方向走最多能走多少步。每次二分找到接下来的第一个拐点搜下去即可。接下来证明这样做的复杂度是对的。
现在我们从起点开始走。假设是左右。考虑左右都遇到拐点之后,两边都往上走。设左边拐到值 \(x\),右边拐到值 \(y\) 的路上(设 \(x < y\))。接下来假设 \(x\) 遇到了值更大的 \(w\),要拐弯了。若 \(w < y\),则 \(x\) 拐完了会直接走到 \(y\) 上面去(因中间的一定不会拐(即使由于是起点拐了也不会影响,因为后面还是一样)),然后两路就归一了。若 \(w > y\),则 \(y\) 也会拐,则两边都拐之后一共还是只有两个本质不同的东西在走,一个往左,一个往右。于是所有情况下一共只会有四个本质不同的分支在搜。若将这四个分支所在点围成矩形,则容易发现矩形一直在不断扩张,而矩形的最终大小存在限制,因此扩张次数也有上界,因此这个复杂度就是对的。
代码
#include <iostream>
#include <algorithm>
#include <cassert>
#include <array>
#include <map>
#define int long long
using namespace std;
int n, m, q;
struct ST {
int mx[18][50005];
void Build(int *a, int N) {
for (int i = 1; i <= N; i++) mx[0][i] = a[i];
for (int i = 1; (1 << i) <= N; i++) {
for (int j = 1; j + (1 << i) - 1 <= N; j++)
mx[i][j] = max(mx[i - 1][j], mx[i - 1][j + (1 << (i - 1))]);
}
}
int qmax(int l, int r) {
int k = 31 - __builtin_clz(r - l + 1);
return max(mx[k][l], mx[k][r - (1 << k) + 1]);
}
} s1, s2;
map<array<int, 3>, int> mp;
int v1[50005], v2[50005];
// 0123 RDLU
int dfs(int x, int y, int d) {
assert(x != -1 && y != -1);
if (mp.count({ x, y, d }))
return mp[{ x, y, d }];
int ret = 0;
if (d == 0) {
if (y == m) ret = 0;
else if (s2.qmax(y + 1, m) < v1[x]) ret = m - y;
else {
int l = y + 1, r = m, mid, ans = -1;
while (l <= r) {
mid = (l + r) >> 1;
if (s2.qmax(y + 1, mid) > v1[x]) ans = mid, r = mid - 1;
else l = mid + 1;
}
ret = max(dfs(x, ans, 1), dfs(x, ans, 3)) + ans - y;
}
} else if (d == 2) {
if (y == 1) ret = 0;
else if (s2.qmax(1, y - 1) < v1[x]) ret = y - 1;
else {
int l = 1, r = y - 1, mid, ans = -1;
while (l <= r) {
mid = (l + r) >> 1;
if (s2.qmax(mid, y - 1) > v1[x]) ans = mid, l = mid + 1;
else r = mid - 1;
}
ret = max(dfs(x, ans, 1), dfs(x, ans, 3)) + y - ans;
}
} else if (d == 1) {
if (x == n) ret = 0;
else if (s1.qmax(x + 1, n) < v2[y]) ret = n - x;
else {
int l = x + 1, r = n, mid, ans = -1;
while (l <= r) {
mid = (l + r) >> 1;
if (s1.qmax(x + 1, mid) > v2[y]) ans = mid, r = mid - 1;
else l = mid + 1;
}
ret = max(dfs(ans, y, 0), dfs(ans, y, 2)) + ans - x;
}
} else {
if (x == 1) ret = 0;
else if (s1.qmax(1, x - 1) < v2[y]) ret = x - 1;
else {
int l = 1, r = x - 1, mid, ans = -1;
while (l <= r) {
mid = (l + r) >> 1;
if (s1.qmax(mid, x - 1) > v2[y]) ans = mid, l = mid + 1;
else r = mid - 1;
}
ret = max(dfs(ans, y, 0), dfs(ans, y, 2)) + x - ans;
}
}
return mp[{ x, y, d }] = ret;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) cin >> v1[i];
for (int i = 1; i <= m; i++) cin >> v2[i];
s1.Build(v1, n), s2.Build(v2, m);
while (q--) {
int x, y;
cin >> x >> y;
cout << max({ dfs(x, y, 0), dfs(x, y, 1), dfs(x, y, 2), dfs(x, y, 3) }) << "\n";
}
return 0;
}
K.City
鬼题,还不会。
L.Dragon 2
计算几何。不写。
分组的思想。
E:这一组东西不够用,通过考虑后面的东西把限制转移到后面去。
反复横跳的倍增。区间树。
一类交互:维护当前已知信息集合。
从简单的情况开始讨论。
平衡思想。
两集合中元素相互询问,通过枚举小侧转化为 单点 对集合询问,再加记忆化,代价根号。

浙公网安备 33010602011771号