JOISC2016
A. Matryoshka
发现所求实际上是最小链覆盖。Dilworth 定理变成最长反链。这里的最长反链实际上可以视为最长下降子序列。然后每次询问的又是右下矩形,因此可以离线下来倒着扫然后随便拿个数据结构维护一下就好了。
代码
#include <iostream>
#include <algorithm>
#define lowbit(x) ((x) & (-(x)))
using namespace std;
int n, q;
pair<int, int> p[200005];
struct Query {
int first, second, id;
} qs[200005];
int d[400005], dcnt;
struct BIT {
int bit[400005];
void add(int x, int y) { for (; x <= 400000; x += lowbit(x)) bit[x] = max(bit[x], y); }
int query(int x) {
int ret = 0;
for (; x; x -= lowbit(x)) ret = max(ret, bit[x]);
return ret;
}
} bit;
int ans[200005];
int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> p[i].first >> p[i].second, d[++dcnt] = p[i].second;
sort(p + 1, p + n + 1, [](pair<int, int> a, pair<int, int> b) { return a.first == b.first ? (a.second < b.second) : (a.first > b.first); });
for (int i = 1; i <= q; i++) cin >> qs[i].first >> qs[i].second, d[++dcnt] = qs[i].second, qs[i].id = i;
sort(qs + 1, qs + q + 1, [](Query a, Query b) { return a.first == b.first ? (a.second > b.second) : (a.first > b.first); });
sort(d + 1, d + dcnt + 1);
dcnt = unique(d + 1, d + dcnt + 1) - d - 1;
for (int i = 1; i <= n; i++) p[i].second = lower_bound(d + 1, d + dcnt + 1, p[i].second) - d;
for (int i = 1; i <= q; i++) qs[i].second = lower_bound(d + 1, d + dcnt + 1, qs[i].second) - d;
for (int i = 1, j = 1; i <= q; i++) {
while (p[j].first >= qs[i].first) bit.add(p[j].second, bit.query(p[j].second) + 1), ++j;
ans[qs[i].id] = bit.query(qs[i].second);
}
for (int i = 1; i <= q; i++) cout << ans[i] << "\n";
return 0;
}
B. Memory2
正解好像比较繁琐,我写的是期望正确的随机化算法,也在官方题解 ppt 中作为另解被提及。
考虑我们随便拿一个元素出来,和其他所有元素询问一遍。考虑询问结果的众数,如果其出现次数超过 \(2\),那么可以发现所有其他询问得到的都是真值。而对于得到询问结果众数的这些询问,我们把这些东西拎出来,和我们拿来和其他元素问的东西并到一起,再随机在其中拿出一个东西和里面的所有东西都问一遍,然后再找到众数,一直这样做下去,直到众数的出现次数为 \(2\),这个时候所有东西就确定了,而我们拿来询问的那个东西就等于那个只出现了一次的询问结果(容易发现这时候一定存在)。那么这样做,每次期望会减少一半的元素,因此总询问次数期望为 \(4N\)。
代码
#include <iostream>
#include <algorithm>
#include <cassert>
#include <vector>
#include <random>
#include "Memory2_lib.h"
using namespace std;
random_device rd;
mt19937 mtrand(rd());
int n;
int ans[105], rec[105];
bool dtm[105];
vector<int> vec[105];
void solve(vector<int> V) {
for (int i = 0; i < n; i++) vec[i].clear();
int m = V.size();
shuffle(V.begin(), V.end(), mtrand);
for (int i = 1; i < m; i++) vec[Flip(V[0], V[i])].emplace_back(V[i]);
for (int i = 0; i < n; i++) {
if (vec[i].size() & 1)
vec[i].emplace_back(V[0]);
}
for (int i = 0; i < n; i++) {
if ((int)vec[i].size() == 2)
rec[vec[i][0]] = rec[vec[i][1]] = i;
}
for (int i = 0; i < n; i++) {
if ((int)vec[i].size() > 2)
solve(vec[i]);
}
}
void Solve(int T, int n) {
::n = n;
for (int i = 0; i < n * 2; i++) vec[n].emplace_back(i);
solve(vec[n]);
for (int i = 0; i < n; i++) vec[i].clear();
for (int i = 0; i < n * 2; i++) vec[rec[i]].emplace_back(i);
for (int i = 0; i < n; i++) Answer(vec[i][0], vec[i][1], i);
}
C. Solitaire
首先发现每一个连通块都是独立的,对每个连通块分别处理,最后组合数合并。每个连通块一定是中间一行全部没填,然后上下有一些位置没填。我们考虑按列 dp,按顺序考虑每个中间位置。对于每个中心位置,我们钦定它如果能用列放置就用列放置,否则用行放置。设 \(f_{i, 0 / 1}\) 表示前 \(i\) 列,最后一列的中心位置是行还是列放置。由于有的时候需要保证中心位置能进行行放置,我们还需要知道相邻两列中心位置放置的顺序。因此需要再加一位表示最后一列的中心位置是在所有空格里第几个放置的。\(f_{i, j, 0 / 1}\) 表示考虑了前 \(i\) 列,第 \(i\) 列的中心位置是所有空格里第 \(j\) 个放置的,用的是行放置(\(0\))还是列放置(\(1\))。
然后考虑转移,分三种情况讨论:
-
\(1 \rightarrow 1\):只需要保证在放第 \(i\) 列的中心格时其上下两个都已经放好。
-
\(0 \rightarrow 1\):需要保证第 \(i\) 列的中心格在第 \(i - 1\) 列的中心格之前放置,且第 \(i\) 列的上下两格在中心格放置之前被放置。
-
\(1 \rightarrow 0\):需要保证第 \(i\) 列的中心格在第 \(i - 1\) 列的中心格之后放置,且第 \(i\) 列的上下两格不能都在中心格之前被放置。
三种情况分别可以列出方程,其中第三种较复杂。三种情况都可以使用前缀和优化转移,于是复杂度就是 \(\mathcal{O}(n^2)\)。注意如果连通块的第一列是棋盘的第一列,则这一列不能用行放置。棋盘最后一列同理。
关于无解的情况,要么是原棋盘上的四角有空格,要么是第一三行存在空个构成的长度 \(\ge 2\) 的连续段,其他情况都有解。一三行上的孤立空格可以任意顺序放,阶乘一下用组合数插到总方案中即可。
代码
#include <iostream>
#include <string.h>
#define int long long
using namespace std;
const int P = 1000000007;
inline void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
int n;
string str[3];
int f[2005][6005][2]; // 1 : column 0 : row
int C[6005][6005], fac[6005];
int p[6010][3];
int cnt;
int work(int l, int r) {
// use column if can
int cnt;
cnt = (str[0][l] == 'x') + (str[1][l] == 'x') + (str[2][l] == 'x');
f[l][cnt][1] = fac[cnt - 1];
if (l != 1) {
if (cnt == 2)
f[l][1][0] = 1;
if (cnt == 3)
f[l][1][0] = f[l][2][0] = 2;
}
memset(p, 0, sizeof p);
for (int j = 1; j <= cnt + 5; j++) p[j][1] = (p[j - 1][1] + f[l][j][1]) % P;
for (int j = 1; j <= cnt + 5; j++) p[j][2] = (p[j - 1][2] + j * f[l][j][1]) % P;
for (int j = cnt; j; j--) p[j][0] = (p[j + 1][0] + f[l][j][0]) % P;
for (int i = l + 1; i <= r; i++) {
int t = (str[0][i] == 'x') + (str[2][i] == 'x');
cnt += t + 1;
for (int j = 1; j <= cnt; j++) {
// 1 -> 1
int coe = fac[t] * C[j - 1][t] % P;
Madd(f[i][j][1], p[cnt - t - 1][1] * coe % P);
// 0 -> 1
Madd(f[i][j][1], p[j - t][0] * coe % P);
if (t) {
// 1 -> 0
Madd(f[i][j][0], p[j - 1][1] * C[cnt - j][t] % P * fac[t] % P);
if (t != 1) {
coe = (cnt - j) * fac[t] % P;
Madd(f[i][j][0], (p[j - 1][1] * (j - 1) - p[j - 1][2] + P) % P * coe % P);
if (j >= 2)
Madd(f[i][j][0], p[j - 2][2] * coe % P);
}
}
}
for (int j = 1; j <= cnt + 5; j++) p[j][1] = (p[j - 1][1] + f[i][j][1]) % P;
for (int j = 1; j <= cnt + 5; j++) p[j][2] = (p[j - 1][2] + j * f[i][j][1]) % P;
for (int j = cnt; j; j--) p[j][0] = (p[j + 1][0] + f[i][j][0]) % P;
}
::cnt += cnt;
int ret = 0;
for (int i = 1; i <= cnt; i++) Madd(ret, (f[r][i][1] + (r != n) * f[r][i][0]) % P);
return ret % P;
}
signed main() {
cin >> n;
for (int i = C[0][0] = fac[0] = 1; i <= n * 3; i++) {
fac[i] = fac[i - 1] * i % P;
for (int j = C[i][0] = 1; j <= i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
}
cin >> str[0] >> str[1] >> str[2];
str[0] = ' ' + str[0], str[1] = ' ' + str[1], str[2] = ' ' + str[2];
if (str[0][1] == 'x' || str[0][n] == 'x' || str[2][1] == 'x' || str[2][n] == 'x')
return cout << "0\n", 0;
int c = 0, mx = 0;
for (int i = 1; i <= n; i++) (str[0][i] == 'x') ? (mx = max(mx, ++c)) : (c = 0);
if (mx > 1)
return cout << "0\n", 0;
for (int i = 1; i <= n; i++) (str[2][i] == 'x') ? (mx = max(mx, ++c)) : (c = 0);
if (mx > 1)
return cout << "0\n", 0;
int _ = 0, ans = 1;
for (int i = 1; i <= n; i++) {
if (str[1][i] == 'o')
_ += (str[0][i] == 'x') + (str[2][i] == 'x');
else {
int t = cnt;
int j = i;
while (str[1][j] == 'x') ++j;
ans = ans * work(i, j - 1) % P * C[cnt][t] % P;
i = j - 1;
}
}
cout << ans * C[cnt + _][_] % P * fac[_] % P << "\n";
return 0;
}
D. Employment
考虑询问的值从 \(0\) 不断增加的过程,显然最开始答案为 \(1\),接下来考虑询问的值增加 \(1\),那么有一些元素就不在了,这个时候如果其小于它两边的相邻元素,则观察到答案会增加 \(1\)。若其大于两边的相邻元素,则答案会减少 \(1\)。否则答案不变。因此可以对每个元素分开考虑其对答案的贡献。我们只需要开一个数据结构维护每个询问值对应的答案,每次改变元素的时候考虑这个元素的改变对答案的影响即可。注意相邻元素可以相等,这个时候可以开一个 set 维护同色连续段之类的。应该有更好写的做法。
代码
#include <iostream>
#include <algorithm>
#include <set>
#define lowbit(x) ((x) & (-(x)))
using namespace std;
int n, m;
int a[200005];
int d[400005], dcnt;
int op[200005], opx[200005], opy[200005];
int c[200005];
struct BIT {
int bit[400005];
void add(int x, int y = 1) { for (; x <= 400000; x += lowbit(x)) bit[x] += y; }
int query(int x) {
int ret = 0;
for (; x; x -= lowbit(x)) ret += bit[x];
return ret;
}
} bit;
set<int> st;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i], d[++dcnt] = a[i];
for (int i = 1; i <= n; i++) {
if (a[i] != a[i + 1])
st.insert(i);
}
st.insert(0), st.insert(n + 1);
st.insert(-1), st.insert(n + 2);
for (int i = 1; i <= m; i++) {
cin >> op[i];
if (op[i] == 1)
cin >> opx[i], d[++dcnt] = opx[i];
else
cin >> opx[i] >> opy[i], d[++dcnt] = opy[i];
}
sort(d + 1, d + dcnt + 1);
dcnt = unique(d + 1, d + dcnt + 1) - d - 1;
for (int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + dcnt + 1, a[i]) - d;
auto upd = [&](set<int>::iterator it, int c) {
if (*it <= 0 || *it > n)
return;
int t = (a[*prev(it)] < a[*it]) + (a[*next(it)] < a[*it]);
if (t == 2)
bit.add(a[*it] + 1, -c);
else if (t == 0)
bit.add(a[*it] + 1, c);
};
for (set<int>::iterator it = st.begin(); it != st.end(); ++it) upd(it, 1);
for (int i = 1; i <= m; i++) {
if (op[i] == 1) {
opx[i] = lower_bound(d + 1, d + dcnt + 1, opx[i]) - d;
cout << 1 + bit.query(opx[i]) << "\n";
} else {
opy[i] = lower_bound(d + 1, d + dcnt + 1, opy[i]) - d;
set<int>::iterator it = st.lower_bound(opx[i]), x = prev(it), y = next(it);
int p = opx[i];
upd(it, -1), upd(x, -1), upd(y, -1);
int s = *prev(x) + 1;
if (st.count(p))
st.erase(st.find(p));
if (st.count(p - 1))
st.erase(st.find(p - 1));
a[p] = opy[i];
if (a[p] != a[p + 1])
st.insert(p);
if (a[p - 1] != a[p])
st.insert(p - 1);
for (it = st.lower_bound(s);; ++it) {
upd(it, 1);
if (it == y)
break;
}
}
}
return 0;
}
E. Sandwich
神人题目。不知道怎么想出来。只好随便写一点意淫的思路。
考虑建边,但是不确定到底要往哪个方向建。考虑如果钦定了一个格子从什么方向取,则容易发现对应方向都一定要取到底,取到边界。因此从边界考虑,考虑直接枚举一列,从上往下一次考虑每个格子,并钦定当前格子从上往下被取走,然后直接搜,会发现搜到的所有格子的建边方向在被搜到的时候就确定了。因此可以求出这个格子的所有前置格子。而且容易发现随着我们考虑的格子往下,前置格子的集合是单调变大的。因此枚举到新的格子时直接在前一个格子搜完的基础上继续搜即可。记忆化,这样对每一列都会搜一遍整张图,复杂度三次方。当然每一列也要从下往上再扫一遍。
代码
#include <iostream>
#include <string.h>
using namespace std;
int n, m;
string str[405];
bool ins[405][405], vis[405][405], no;
int cnt = 0;
int ans[405][405];
// d : 0123, from LURD
void dfs(int x, int y, int d) {
if (!x || !y || x > n || y > m)
return;
if (ins[x][y])
return no = 1, void();
if (!vis[x][y])
cnt += 2;
else
return;
ins[x][y] = 1;
vis[x][y] = 1;
((d == 2 || d == 3) && str[x][y] == 'Z') ? (dfs(x - 1, y, 3), dfs(x, y - 1, 2)) : void();
((d == 1 || d == 2) && str[x][y] == 'N') ? (dfs(x + 1, y, 1), dfs(x, y - 1, 2)) : void();
((d == 0 || d == 1) && str[x][y] == 'Z') ? (dfs(x + 1, y, 1), dfs(x, y + 1, 0)) : void();
((d == 0 || d == 3) && str[x][y] == 'N') ? (dfs(x - 1, y, 3), dfs(x, y + 1, 0)) : void();
ins[x][y] = 0;
}
int main() {
memset(ans, 63, sizeof ans);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> str[i], str[i] = ' ' + str[i];
for (int i = 1; i <= m; i++) {
cnt = 0; no = 0;
memset(ins, 0, sizeof ins);
memset(vis, 0, sizeof vis);
for (int j = 1; j <= n; j++) {
if (vis[j][i])
break;
dfs(j, i, 3);
if (no)
break;
ans[j][i] = min(ans[j][i], cnt);
}
}
for (int i = 1; i <= m; i++) {
cnt = 0; no = 0;
memset(ins, 0, sizeof ins);
memset(vis, 0, sizeof vis);
for (int j = n; j; j--) {
if (vis[j][i])
break;
dfs(j, i, 1);
if (no)
break;
ans[j][i] = min(ans[j][i], cnt);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
cout << (ans[i][j] == 0x3f3f3f3f ? -1 : ans[i][j]) << " \n"[j == m];
}
return 0;
}
F. Toilets
?怎么我想到了变成 \(\pm1\),也想到了从后面考虑,但没想到把这俩拼起来?
考虑把女孩子设为 \(+1\),男的设为 \(-1\),则一个序列合法的充要条件是其所有后缀和 \(\ge -1\)。
实际上这是非常巧妙的。也许可以考虑到女孩子后面的男性不能找到前面的女孩子,但是男性后面的女孩子可以抵消前面的男性。也就是如果从后面往前考虑,那么一个女孩子相当于一个提前量,如果一个时刻的后缀和是 \(-1\),那么如果前面是一个女孩子,那还能救回来,否则若前缀和 \(< -1\),那么无论如何由于前面的女孩子不能匹配后面的男性,那么这个序列不合法。因此这个就是一个充要的条件。
那么有了这个条件,我们再贪心地考虑交换。将所有后缀和画成折线图。从后往前,如果当前位置的后缀和挂了,那么我们应当在前面找到一个最近的女孩子,把她拉过来。容易发现这样操作之后这个地方一定变成男女交替。那么我们把女孩子拉过来这个操作,需要的代价,容易发现若当前位置的后缀和恰好为 \(-2\),则是当前这个 \(-2\) 处于的谷的最低点的纵坐标,的绝对值,减一。那么又由于从后往前做,遇到不合法的地方我们一定会就地正法,于是我们遇到的每个不合法的地方一定都是后缀和为 \(-2\) 的地方,从而最大代价也就是整个序列的最低点的绝对值减一。显然这是容易求的。时间复杂度线性。
代码
#include <iostream>
#define int long long
using namespace std;
int n, m;
string str[100005];
int rep[100005];
int s[100005], vl[100005];
signed main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) cin >> str[i] >> rep[i];
int mn = -1, S = 0;
for (int i = m; i; i--) {
for (int j = (int)str[i].size() - 1; ~j; j--)
s[i] += (str[i][j] == 'F' ? 1 : -1), vl[i] = min(vl[i], s[i]);
mn = min(mn, vl[i] + S);
mn = min(mn, vl[i] + (rep[i] - 1) * s[i] + S);
S += s[i] * rep[i];
}
if (S < 0)
cout << "-1\n";
else
cout << -mn - 1 << "\n";
return 0;
}
G. Dungeon 2
显然可以 dfs。观察 dfs 树,发现只有返祖边。将 dfs 树按 dfs 序重编号之后,我们只需要知道每条返祖边到底连的是啥。发现在 dfs 过程中,dfs 栈里的点,深度和标号构成双射。因此我们考虑将点的深度信息三进制拆分记在颜色上。我们先用一次 dfs 对所有点求出其所有边的类型(返祖,前向,树边),这样之后的 dfs 都只需要访问每条边恰好两次。接下来我们进行五次 dfs(\(3^5 \ge 200\)),第 \(i\) 次在这个点的颜色上记录其深度的第 \(i\) 个三进制位,然后前向边不管,我们在返祖边上统计连边,这样我们就可以求出每个点到底连向了它的哪些祖先。从而可以建出图来,于是就做完了。
代码
#include "dungeon2.h"
#include <iostream>
#include <string.h>
using namespace std;
int n;
int t[205][205], deg[205], in[205];
int g[205][205];
// > 0 : tree edge
// -1 : back-to-fa edge
// -2 : to-son edge
int dfs0() {
int c = Color();
if (c == 2)
return -1;
else if (c == 3)
return -2;
int cur = ++n;
in[cur] = LastRoad();
int x = cur;
deg[cur] = NumberOfRoads();
for (int i = 1; i <= deg[cur]; i++) {
if (i == in[cur])
continue;
Move(i, 2);
int back = LastRoad();
t[x][i] = dfs0();
if (t[x][i] > 0)
g[t[x][i]][x] = g[x][t[x][i]] = 1;
Move(back, t[x][i] != -1 ? 3 : 2);
}
return cur;
}
int td[205][205];
int stk[205], sz = -1;
void dfs(int x, int c, int lay, int lim, int cnt) {
stk[++sz] = x;
int nc = (cnt == lim ? (c % 3 + 1) : c);
for (int i = 1; i <= deg[x]; i++) {
if (t[x][i] > 0) {
Move(i, c);
dfs(t[x][i], nc, lay, lim, ((nc == c) ? (cnt + 1) : 1));
} else if (t[x][i] == -1) {
Move(i, c);
int tmp = LastRoad(), tc = Color();
td[x][i] += lim * (tc - 1);
if (lay == 5) {
int tx = stk[td[x][i]];
g[tx][x] = g[x][tx] = 1;
}
Move(tmp, tc);
}
}
--sz;
if (in[x] != -1)
Move(in[x], 1);
}
int cnt[205];
void Inspect(int R) {
memset(g, 63, sizeof g);
dfs0();
for (int i = 1, X = 1; i <= 5; i++, X *= 3) {
dfs(1, 1, i, X, 1);
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++)
++cnt[g[i][j]];
}
for (int i = 1; i <= R; i++) Answer(i, cnt[i]);
}
H. Sushi
良心 joi。不卡常。
考虑操作在干什么。实际上相当于找到区间第一个比这个大的,然后依次依次往后找,然后同时把所有东西放到它后面第一个比它大的位置上。最后一个就删掉。发现这个事情根本没有什么东西可以维护,考虑分块。我们发现对一整块做操作,最后出来的一定是块内最大值。于是这部分对每块开优先队列维护,容易做。接下来考虑散块。对于散块,我们遇到的问题是我们虽然能知道一些东西经过之后剩了哪些东西,但并不能知道这些东西的位置。我们需要快速求出做掉这些操作之后每个位置上是什么。那么这个时候我们考虑第一个位置,把所有操作依次移过来,那么会发现这个位置最后一定得到的是这些操作以及原值中的最小值。那么考虑这个过程,就会发现所有操作做过来的顺序并不重要。于是这部分开个优先队列存一下每个整块堆下来的操作,等到要用到了再把这个块重构。总时间复杂度 \(\mathcal{O}(n\sqrt{n}\log n)\)。良心 joi,时限直接开到了九秒。赞美 joi。
代码
#include <iostream>
#include <math.h>
#include <queue>
using namespace std;
int S, T;
int n, q;
int a[400005];
int bel[400005];
int L[10005], R[10005];
priority_queue<int> q_a[400005];
priority_queue<int, vector<int>, greater<int> > q_q[400005];
void Re(int p) {
while (q_a[p].size()) q_a[p].pop();
for (int i = L[p]; i <= R[p]; i++) q_a[p].push(a[i]);
}
void ReBuild(int p) {
for (int i = L[p]; i <= R[p]; i++) {
q_q[p].push(a[i]);
a[i] = q_q[p].top(), q_q[p].pop();
}
while (q_q[p].size()) q_q[p].pop();
Re(p);
}
int work(int l, int r, int v) {
for (int i = l; i <= r; i++) {
if (a[i] > v)
swap(a[i], v);
}
Re(bel[l]);
return v;
}
int Query(int l, int r, int v) {
if (bel[l] == bel[r]) {
ReBuild(bel[l]);
return work(l, r, v);
}
int pl = bel[l], pr = bel[r];
ReBuild(pl), ReBuild(pr);
int t = work(l, R[pl], v);
for (int i = pl + 1; i < pr; i++) {
q_q[i].push(t), q_a[i].push(t);
t = q_a[i].top(), q_a[i].pop();
}
return work(L[pr], r, t);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> q;
S = ceil(sqrt(n));
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i += S) {
++T;
for (int j = i; j < min(n + 1, i + S); j++) bel[j] = T, q_a[T].push(a[j]);
L[T] = i, R[T] = min(i + S - 1, n);
}
while (q--) {
int l, r, x;
cin >> l >> r >> x;
if (l <= r)
cout << Query(l, r, x) << "\n";
else
cout << Query(1, r, Query(l, n, x)) << "\n";
}
return 0;
}
I. Telegraph
由于一个点改了就改了,改之后的边可以任连,所以我们只需要把原图通过断掉一些边拆成若干链即可。那么对于所有树部分的点,如果有多个儿子,那一定保留代价最大的。对于环,每个位置要么保留环边把别的全断干净了,要么断掉环边,保留剩下的最大代价。对于一个环,我们需要至少一个位置选择断掉环边,写个小 dp 即可。注意若原图整个就是一个环,那此时不需要任何代价。
代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#define int long long
using namespace std;
int n;
int a[100005], c[100005];
vector<int> G[100005];
int ind[100005], v2[100005], s[100005], stk[100005], to[100005], sz;
bool mark[100005];
queue<int> q;
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i] >> c[i], G[a[i]].emplace_back(i), ++ind[a[i]];
for (int i = 1; i <= n; i++) {
if (!ind[i])
q.push(i);
}
while (q.size()) {
int x = q.front();
q.pop();
--ind[a[x]];
if (!ind[a[x]])
q.push(a[x]);
}
for (int i = 1; i <= n; i++) {
v2[i] = 0;
for (int v : G[i]) {
if (!ind[v])
v2[i] = max(v2[i], c[v]), s[i] += c[v];
else
to[i] = v;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (!ind[i])
ans += s[i] - v2[i];
else if (!mark[i]) {
int x = a[i]; sz = 0;
do stk[++sz] = x, mark[x] = 1, x = a[x];
while (x != a[i]);
if (sz == n) {
cout << "0\n";
return 0;
}
int f0 = 0, f1 = 0x3f3f3f3f3f3f3f3f;
for (int j = 1; j <= sz; j++) {
int v0 = s[stk[j]], v1 = c[to[stk[j]]] + s[stk[j]] - v2[stk[j]];
int t0 = f0 + v0, t1 = min({ f0 + v1, f1 + v0, f1 + v1 });
f0 = t0, f1 = t1;
}
ans += f1;
}
}
cout << ans << "\n";
return 0;
}
J. Dangerous Skating
神金题目。
就,想点有道理的东西,会发现唯一看起来有点道理的移动方式是你如果想往某个方向移动到某个位置,那你肯定是往这个方向走过去走回来走过去走回来,每次过去回来都能往目标方向移动一个距离。然后你考虑从起点走到终点的过程,不难发现你移动过程中产生的新的障碍一定不会被用作之后移动的障碍,因为否则你不如这一次就不要走那么多。那么这样我们就只需要考虑当前位置即可。然后考虑建边。把每个点向相邻的点连边权为 \(2\) 的边,把它向四向各遇到的第一个冰块连边权为 \(1\) 的边。容易发现这样就能够很好地刻画所有形式的移动。(???到底是什么人能想出来这种东西???)
代码
#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
const int inf = 0x3f3f3f3f;
int n, m;
string str[1005];
inline int id(int x, int y) { return (x - 1) * m + y; }
int head[1000005], nxt[10000005], to[10000005], ecnt;
bool ew[10000005];
void add(int u, int v, int ww) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, ew[ecnt] = ww; }
int dist[1000005];
bool vis[1000005];
struct node {
int x, dis;
} tmp;
inline bool operator<(node a, node b) { return a.dis > b.dis; }
priority_queue<node> q;
void dijkstra(int S) {
memset(dist, 63, sizeof dist);
q.push((node) { S, dist[S] = 0 });
while (q.size()) {
tmp = q.top();
q.pop();
int x = tmp.x;
if (vis[x])
continue;
vis[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (dist[v] > dist[x] + ew[i] + 1)
q.push((node) { v, dist[v] = dist[x] + ew[i] + 1 });
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> str[i], str[i] = ' ' + str[i];
for (int i = 1; i <= n; i++) {
int lst = 0;
for (int j = 1; j <= m; j++) {
if (str[i][j] == '#')
lst = j;
else {
if (lst + 1 != j)
add(id(i, j), id(i, lst + 1), 0);
if(str[i][j - 1] != '#')
add(id(i, j), id(i, j - 1), 1);
}
}
for (int j = m; j; j--) {
if (str[i][j] == '#')
lst = j;
else {
if (lst - 1 != j)
add(id(i, j), id(i, lst - 1), 0);
if (str[i][j + 1] != '#')
add(id(i, j), id(i, j + 1), 1);
}
}
}
for (int j = 1; j <= m; j++) {
int lst = 0;
for (int i = 1; i <= n; i++) {
if (str[i][j] == '#')
lst = i;
else {
if (lst + 1 != i)
add(id(i, j), id(lst + 1, j), 0);
if (str[i - 1][j] != '#')
add(id(i, j), id(i - 1, j), 1);
}
}
for (int i = n; i; i--) {
if (str[i][j] == '#')
lst = i;
else {
if (lst - 1 != i)
add(id(i, j), id(lst - 1, j), 0);
if (str[i + 1][j] != '#')
add(id(i, j), id(i + 1, j), 1);
}
}
}
int sx, sy, tx, ty;
cin >> sx >> sy >> tx >> ty;
dijkstra(id(sx, sy));
int ans = dist[id(tx, ty)];
cout << (ans == inf ? -1 : ans) << "\n";
return 0;
}
K. Snowy Roads
Anya 这个名字念起来好可爱
考虑仿照分块,把所有点按深度模 \(12\) 分组,选择最小的组,把其中所有点作为关键点。然后每个关键点直接存答案,然后再把所有边的状态存下来。这样 Anya 一共需要 \(\frac{500}{12} \times 9 + 500 < 1000\) 个 bit,而 B 每次查询,只需要找到上面第一个关键点。由于我们按深度模 \(12\) 分了组,所以这一步最多需要问 \(11\) 条边。到了关键点之后我们直接问答案,这一步需要 \(9\) 个 bit 的询问。于是我们总共刚好使用了 \(20\) 个 bit,可以通过。
代码
// #include "Anyalib.h"
// #include "Borislib.h"
#include "snowy.h"
#include <iostream>
#include <string.h>
#include <cassert>
using namespace std;
namespace Anyaa {
const int X = 12;
int n;
int fa[505], in[505];
int dep[505], lst[505], cnt[505];
int key[505];
int head[505], nxt[1005], to[1005], ecnt;
int ew[1005];
int pre[505];
void add(int u, int v, int ww) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, ew[ecnt] = ww; }
void dfs(int x, int f) {
fa[x] = f;
++cnt[dep[x] = (dep[f] + 1) % X];
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != f) {
in[v] = ew[i];
dfs(v, x);
}
}
}
void dfs2(int x, int y) {
lst[x] = (key[x] ? x : y);
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa[x])
dfs2(v, lst[x]);
}
}
void dfs(int x, int c[]) {
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa[x]) {
pre[v] = pre[x] + c[ew[i]];
dfs(v, c);
}
}
}
}
void InitAnya(int N , int A[] , int B[]) {
using namespace Anyaa;
n = N;
memset(cnt, 0, sizeof cnt);
for (int i = 0; i < N - 1; i++) add(A[i], B[i], i), add(B[i], A[i], i);
dfs(0, 0);
int mnp = 0;
for (int i = 1; i < X; i++) (cnt[i] < cnt[mnp]) ? (mnp = i) : 0;
for (int i = 0; i < n; i++) key[i] = (dep[i] == mnp);
key[0] = 1;
dfs2(0, 0);
int c = 0;
for (int i = 0; i < n; i++) c += key[i];
assert(c <= 50);
}
void Anya(int C[]) {
using namespace Anyaa;
dfs(0, C);
for (int i = 0; i < n - 1; i++) Save(i, C[i]);
int cur = 500;
for (int i = 0; i < n; i++) {
if (key[i]) {
for (int j = 0; j < 9; j++)
Save(cur++, (pre[i] >> j) & 1);
}
}
}
namespace boris {
const int X = 12;
int fa[505], in[505], n;
int dep[505], lst[505], cnt[505];
int key[505];
int head[505], nxt[1005], to[1005], ecnt;
int ew[1005];
void add(int u, int v, int ww) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, ew[ecnt] = ww; }
void dfs(int x, int f) {
fa[x] = f;
++cnt[dep[x] = (dep[f] + 1) % X];
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != f) {
in[v] = ew[i];
dfs(v, x);
}
}
}
void dfs2(int x, int y) {
lst[x] = (key[x] ? x : y);
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa[x])
dfs2(v, lst[x]);
}
}
}
void InitBoris(int N , int A[] , int B[]) {
using namespace boris;
n = N;
memset(cnt, 0, sizeof cnt);
for (int i = 0; i < N - 1; i++) add(A[i], B[i], i), add(B[i], A[i], i);
dfs(0, 0);
int mnp = 0;
for (int i = 1; i < X; i++) (cnt[i] < cnt[mnp]) ? (mnp = i) : 0;
for (int i = 0; i < n; i++) key[i] = (dep[i] == mnp);
key[0] = 1;
dfs2(0, 0);
}
int Boris(int city) {
using namespace boris;
int x = city, ret = 0;
while (!key[x]) {
ret += Ask(in[x]);
x = fa[x];
}
int cur = 500;
for (int i = 0; i < x; i++) {
if (key[i])
cur += 9;
}
for (int i = 0; i < 9; i++) ret += (Ask(cur + i) << i);
return ret;
}
L. Worst Reporter 2
首先我们希望尽量多地匹配当前国籍相同的且能匹配的人。那么这些匹配完之后,剩下的那些人,首先我们要保证剩下的这些东西有解,其次必须把这些人的国籍挨个全改一遍,才可以合法。因此我们的目标就是匹配尽量多的当前国籍相同的人,并要使得剩下的东西能够有解。按总成绩升序考虑,Hall 定理告诉我们每个总成绩前缀对应的第一成绩前缀必须长于自己。这个限制对每个总成绩前缀都存在,除非它被匹配走了。那么我们现在考虑依次加入每个总成绩,并对应地加入另一边的第一成绩。这个时候我们尝试匹配当前总成绩。我们找到所有已经被加入的第一成绩里国籍和当前总成绩相同的,如果能找到的话我们看一看这对匹配了之后是否会让前面某些没有匹配的总成绩前缀爆掉,如果不会我们就直接匹配掉。维护前面是否有总成绩爆掉,我们考虑对于每个总成绩前缀,实时维护它对应的第一成绩前缀的长度减掉自己的长度。那么如果设我们当前匹配的第一成绩被加入是在第 \(x\) 个总成绩前缀,那么这个匹配会把从 \(x\) 到当前总成绩前缀的值都减一。那又因为维护的值任何时刻不能在任何位置上出现负数,因此我们需要查询这一段区间的最小值,如果非 \(0\) 我们才可以进行这一对匹配。匹配过后需要把区间值都减一。综上我们需要支持的是区间减区间 \(\min\),直接上线段树即可。
代码
#include <iostream>
#include <vector>
using namespace std;
int n;
int a[200005], b[200005], c[200005], d[200005];
int f[200005], mch;
vector<int> vec[200005];
struct Segment_Tree {
int mn[800005], tg[800005];
void tag(int o, int v) { mn[o] += v, tg[o] += v; }
void pushdown(int o) {
if (!tg[o])
return;
tag(o << 1, tg[o]);
tag(o << 1 | 1, tg[o]);
tg[o] = 0;
}
void Add(int o, int l, int r, int L, int R, int v) {
if (L <= l && r <= R)
return tag(o, v);
pushdown(o);
int mid = (l + r) >> 1;
if (L <= mid)
Add(o << 1, l, mid, L, R, v);
if (R > mid)
Add(o << 1 | 1, mid + 1, r, L, R, v);
mn[o] = min(mn[o << 1], mn[o << 1 | 1]);
}
int Query(int o, int l, int r, int L, int R) {
if (L > R)
return n + 1;
if (L <= l && r <= R)
return mn[o];
pushdown(o);
int mid = (l + r) >> 1;
if (R <= mid)
return Query(o << 1, l, mid, L, R);
if (L > mid)
return Query(o << 1 | 1, mid + 1, r, L, R);
return min(Query(o << 1, l, mid, L, R), Query(o << 1 | 1, mid + 1, r, L, R));
}
} seg;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i];
for (int i = 1; i <= n; i++) cin >> c[i] >> d[i];
for (int i = n, j = n; i; i--) {
while (j && b[j] <= d[i]) {
f[j] = i;
vec[a[j]].emplace_back(j);
--j;
}
seg.Add(1, 1, n, i, i, i - j - 1);
if (vec[c[i]].size() && seg.Query(1, 1, n, i + 1, f[vec[c[i]].back()]) > 0) {
int t = vec[c[i]].back();
vec[c[i]].pop_back();
seg.Add(1, 1, n, i, f[t], -1), ++mch;
seg.Add(1, 1, n, i, i, n + 1);
}
}
cout << n - mch << "\n";
return 0;
}
Dilworth 定理。
图论问题,考虑思考对象。像 E,每次考虑一整列就是容易的,考虑每个格子则困难。也许可以通过分析性质获得提示。
从后往前考虑。\(\pm 1\)。
进制拆分深度,通过无向图的树边和返祖边可以完整描述这张图。
复杂诡异区间操作,考虑分块。
建图过程,考虑类似分步转移。像 J,向四向第一次遇到的冰块连边相当于把从这个点直接到靠那边的代价转化到那一块冰上去。可能也有点类似于改变决策位置吧。也许可以通过观察性质获得提示,比如观察 \(24687531\) 也许可以获得连边方式的启发。用尽量少的方式描述边,必要时改变基准位置。
通信题,重要的是共识。两边平衡的思想,如果每个点记到关键点的距离,则询问少,但记录的多,会爆。因此把这个信息拆开成每条边的信息,这样询问次数刚好顶满。思维不能局限,各种组合都要尝试。
贪心过程中应当考虑可行性。