The 2024 ICPC Asia Nanjing Regional Contest (The 3rd Universal Cup. Stage 16: Nanjing) 题解
The 2024 ICPC Asia Nanjing Regional Contest (The 3rd Universal Cup. Stage 16: Nanjing) 题解
- The 2024 ICPC Asia Nanjing Regional Contest (The 3rd Universal Cup. Stage 16: Nanjing) 题解
- Problem A. Hey, Have You Seen My Kangaroo?
- Problem B. Birthday Gift
- Problem C. Topology
- Problem D. Toe-Tac-Tics
- Problem E. Left Shifting 3
- Problem E. Subway
- Problem G. Binary Tree
- Problem H. Border Jump 2
- Problem I. Bingo
- Problem J. Social Media
- Problem K. Strips
- Problem L. \(P \oplus Q = R\)
- Problem M. Ordainer of Inexorable Judgment
非常难的一场。CF 上给这套题的评级是四颗星。
当我看到金牌线只有六道题的时候就意识到事情的不对劲。
这次有三个不可做题……但其实捧杯线只有九题,所以写不出来……貌似也不影响拿杯?
而且这场的题解质量其实不高,有些式子推的非常省略,不过后面三个题瞟了一下质量还行。
补那三个不可做题的动力不是很充足其实,等我以后水平上来了再补吧。
Problem A. Hey, Have You Seen My Kangaroo?
很唐的一个 Ad-hoc 题,属于是想到了基环树就全部写出来了。
而且内存给的非常充足,有 128MB,只要不犯唐写一个 \(nmk\) 的数组是随便开的。
我们求解问题等价于求解:经过 \(t\) 时间后场上还有多少袋鼠。
我们直接暴力跑一个周期后就可以获得每一个位置在一轮周期后的下一个位置。出度恒定为一可以构成一个基环树森林。
容易发现:如果两只袋鼠会碰到一起,我们定义这个现象为合并,那么这两只袋鼠在基环树上的父亲(或者说下一个节点)是一定相同的。
而且场上出现袋鼠减少这个事情当且仅当出现了合并事件,一个节点在经历了若干轮后是否还有袋鼠仅取决于最长链的长度,环上的点最长链直接视作 \(+\infty\)
不妨对子节点以最长链长度进行排序,统计每个子节点会在一轮新开始的什么时候会合并到前面的节点。以最长链排序可以消除合并贡献的后继性。
我们对基环树森林处理完后可以得知合并操作会在一轮的哪个时间点发生、同时会发生多少轮。这样就可以统计出答案了。
不过我的实现比较劣,复杂度到了 \(O(nm (k + \log{nm} ))\),能过但是不是很优秀,可以看看其他题解的实现。
代码:
#include <bitset>
#include <algorithm>
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 2e5 + 10;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int n, m, k;
std::string s;
std::bitset<(int)(2e5 + 10)> a;
int go[(int)(2e5 + 1)];
std::vector<int> e[(int)(2e5 + 1)];
int id(int x, int y) {
return (x - 1) * m + y - 1;
}
int walk(int x, int y) {
for (auto ch : s) {
if (ch == 'U' && x > 1 && a[id(x - 1, y)]) x--;
if (ch == 'D' && x < n && a[id(x + 1, y)]) x++;
if (ch == 'L' && y > 1 && a[id(x, y - 1)]) y--;
if (ch == 'R' && y < m && a[id(x, y + 1)]) y++;
}
return id(x, y);
}
void init() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[id(i, j)]) {
go[id(i, j)] = walk(i, j);
// 建一个反向边
e[go[id(i, j)]].push_back(id(i, j));
} else {
go[id(i, j)] = -10;
}
}
}
}
std::bitset<(int)(2e5 + 10)> vis;
// 是否在环里面
std::bitset<(int)(2e5 + 10)> inC;
int len[kMaxN];
std::bitset<(int)(2e5 + 1)> tmp;
std::vector<int> cnt[202];
void dfs(int u) {
vis[u] = true;
if (e[u].empty()) return len[u] = 1, void();
for (auto v : e[u]) {
if (inC[v]) continue;
dfs(v);
upmax(len[u], len[v] + 1);
}
std::sort(e[u].begin(), e[u].end(), [](int i, int j) {
return len[i] > len[j];
});
std::vector<std::pair<int, int>> son;
for (auto v : e[u]) son.push_back({v / m + 1, v % m + 1});
std::vector<bool> killed(son.size(), false);
for (int i = 1; i <= k; i++) {
char ch = s[i - 1];
for (int j = 0; j < son.size(); j++) {
auto& [x, y] = son[j];
if (ch == 'U' && x > 1 && a[id(x - 1, y)]) x--;
if (ch == 'D' && x < n && a[id(x + 1, y)]) x++;
if (ch == 'L' && y > 1 && a[id(x, y - 1)]) y--;
if (ch == 'R' && y < m && a[id(x, y + 1)]) y++;
if (!killed[j] && tmp[id(x, y)]) {
killed[j] = true;
// 会在每一轮开始后 i 这个时间点 -1,并且持续至少 len[j] 次
cnt[i].push_back(len[e[u][j]]);
}
tmp[id(x, y)] = true;
}
for (auto& [x, y] : son) {
tmp[id(x, y)] = false;
}
}
}
void set(int u) {
while (!vis[u]) {
vis[u] = true;
u = go[u];
}
int s = u;
inC[s] = true, len[s] = 1e9;
for (u = go[u]; u != s; u = go[u]) {
inC[u] = true, len[u] = 1e9;
}
dfs(s);
for (u = go[u]; u != s; u = go[u]) {
dfs(u);
}
}
int ans[kMaxN];
int main() {
int less = 0;
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m >> k;
cin >> s;
for (int i = 1; i <= n; i++) {
std::string str;
cin >> str;
for (int j = 1; j <= m; j++) {
a[id(i, j)] = str[j - 1] - '0';
less += a[id(i, j)];
}
}
init();
for (int i = 1; i <= n * m; i++) {
if (a[i] == 0) continue;
if (!vis[i]) set(i);
}
for (int i = 1; i <= k; i++) {
std::sort(cnt[i].begin(), cnt[i].end(), [](int i, int j) {
return i > j;
});
}
for (int i = 0; i <= n * m; i++) ans[i] = 1e9;
ans[less] = 0;
for (int t = 1; ; t++) {
bool flag = false;
for (int i = 1; i <= k; i++) {
int u = (t - 1) * k + i;
while (!cnt[i].empty() && cnt[i].back() < t) cnt[i].pop_back();
flag |= !!cnt[i].size();
less -= cnt[i].size();
upmin(ans[less], u);
}
if (!flag) break;
}
for (int i = 1; i <= n * m; i++) {
upmin(ans[i], ans[i - 1]);
cout << (ans[i] == 1e9 ? -1 : ans[i]) << endl;
}
return 0;
}
Problem B. Birthday Gift
纯粹考 trick 的题目,不知道这个 trick 可以写 \(dp\),但是会有非常多繁琐的分类讨论。
我们对 01 字符串的偶数位(奇数位其实也可以)进行 01 反转。
这样我们可以反转邻位判断的性质:比如判断相邻位置不同就可以改成判断相邻位置是否相同。
很逆天的一个 trick,如此简化之后,处理一下原字符串变成形如:0000211112.... 的样子后直接变成统计 012 数量。
代码:
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
void solve() {
std::string str;
cin >> str;
for (int i = 0; i < str.size(); i += 2) {
if (str[i] < '2') str[i] ^= 1;
}
int cnt[4] = {0, 0, 0};
std::string tmp;
for (int i = 0; i < str.size(); i++) {
cnt[str[i] - '0']++;
if (tmp.empty() || tmp.back() == str[i]) {
tmp += str[i];
}
if (str[i] == '2') {
tmp = "";
} else if (!tmp.empty() && tmp.back() != str[i]) {
cnt[tmp.back() - '0']--;
cnt[str[i] - '0']--;
tmp.pop_back();
}
}
int min = std::min(cnt[1], cnt[0]);
int max = std::max(cnt[1], cnt[0]);
// cerr << str << endl;
// cerr << min << ' ' << max << endl;
if (max - min >= cnt[2]) {
cout << max - min - cnt[2] << endl;
} else {
cnt[2] -= (max - min);
cout << (cnt[2] & 1) << endl;
}
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
不过很有意思的是刚才瞟了一下洛谷题解,其实有一种 \(dp\) 的方法。这里只讲状态设计:
\(f_{i, c}\) 为前缀 \(i\) 消除完后末尾数字为 \(c\) 的最短 \(01\) 交替串,同理转移一个最短串 \(g_{i, c}\)。这个状态设计是正确的但是真的很难转移。具体实现看 这位哥们
Problem C. Topology
神秘 \(dp\) 好题,比较卡你的状态设计,如果设计的不够优秀就会死的比较惨。我一开始就设计的很炸裂复杂度给干到 \(O(n^3)\) 去了(大哭
和这个相关的还有一个公式:
对于一个节点树为 \(n\) 的树,记子节点集合 \(V\)、子树大小为 \(S\),它合法的拓扑序数量为:
比较神秘,当然不知道这个公式其实也可以跑 \(DP\) 一点问题都没有,复杂度是一样的。(反正我代码里写的是纯 \(DP\))
现在考虑设计 \(dp\) 状态:\(dp_{u, i}\) 为节点 \(u\)、并且位置放在 \(i\) 的所有方案数。
显然这个转移是不方便的,我们干脆设计一个类似的状态 \(f_{u, i}\),但是此时不考虑 \(u\) 的子树贡献。注意此时的状态设计只是不考虑子树的贡献,所以不代表 \(i\) 就是拓扑序列中的最后一个元素(换而言之 \(u\) 后面还有一坨数字)。
转移是简单显然的:\(f_{u, i}\) 转移到 \(f_{v, j}\),直接考虑 \(u\) 中除了 \(v\) 以外所有子树的合法拓扑序数量,然后合并到原来的拓扑序列上。注意到不论 \(j\) 具体是多少都不会影响我们的方案数(这显然吧),而且 \(f_{u, i}\) 可以转移到 \(f_{u, j + 1}\) 则也可以转移到 \(f_{u, j + 2}\)。所以枚举 \(i\) 后做一个前缀和就可以得到 \(f_{v, j}\) 了。
当然枚举 \(i\) 或者枚举 \(j\) 是存在一个上限的,但是显然不会对答案构成影响,容易证明就自己思考吧。
代码:
#include <cassert>
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 5e3 + 10;
const int MOD = 998244353;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int inc(int x, int y) {
return (x + y >= MOD ? x + y - MOD : x + y);
}
int dec(int x, int y) {
return (x - y < 0 ? x - y + MOD : x - y);
}
int mul(int x, int y) {
return 1ll * x * y % MOD;
}
int pow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = mul(ans, x);
x = mul(x, x);
p >>= 1;
}
return ans;
}
int n;
int fa[kMaxN];
int size[kMaxN];
int fact[kMaxN];
int invfact[kMaxN];
std::vector<int> go[kMaxN];
int C(int n, int r) {
return mul(fact[n], mul(invfact[n - r], invfact[r]));
}
int inv[kMaxN];
// i 为根合法的拓扑序数量
int g[kMaxN];
void dfs(int u) {
g[u] = 1;
for (auto v : go[u]) {
dfs(v);
int s = size[u] + 1;
size[u] += size[v];
g[u] = mul(g[u], mul(g[v], C(s + size[v] - 1, s - 1)));
}
size[u] += 1;
}
// 表示不考虑 u 子树的情况下,dp[u][i] 为 p[u] = i 的方案
int dp[kMaxN][kMaxN];
void dfs2(int u) {
std::vector<int> preg, presize;
std::vector<int> sufg, sufsize;
preg = presize = sufg = sufsize = std::vector<int>(go[u].size(), 0);
// 不用那个公式就是如此朴实无华暴力……
for (int i = 0; i < go[u].size(); i++) {
presize[i] = sufsize[i] = size[go[u][i]];
preg[i] = sufg[i] = g[go[u][i]];
}
for (int i = 1; i < go[u].size(); i++) {
int v = go[u][i];
presize[i] += presize[i - 1];
preg[i] = mul(preg[i - 1], mul(g[v], C(presize[i], size[v])));
}
for (int i = go[u].size() - 2; i >= 0; i--) {
int v = go[u][i];
sufsize[i] += sufsize[i + 1];
sufg[i] = mul(sufg[i + 1], mul(g[v], C(sufsize[i], size[v])));
}
for (int i = 0; i < go[u].size(); i++) {
int v = go[u][i];
int g = 1, size = 0;
if (i == 0 && go[u].size() > 1) {
g = sufg[i + 1], size = sufsize[i + 1];
} else if (i == go[u].size() - 1 && go[u].size() > 1) {
g = preg[i - 1], size = presize[i - 1];
} else if (go[u].size() > 1) {
size = sufsize[i + 1] + presize[i - 1];
g = mul(mul(preg[i - 1], sufg[i + 1]), C(size, sufsize[i + 1]));
}
for (int x = 0; x <= n; x++) {
// another 个节点在 u 后面
int another = n - ::size[u] - x + 1;
if (another < 0) continue;
int tmp = mul(dp[u][x], mul(g, C(another + size, size)));
dp[v][x + 1] = tmp;
}
for (int x = 1; x <= n; x++) {
dp[v][x] = inc(dp[v][x - 1], dp[v][x]);
}
dfs2(v);
}
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
fact[0] = 1, invfact[0] = 1;
for (int i = 1; i < kMaxN; i++) {
inv[i] = pow(i, MOD - 2);
}
for (int i = 1; i < kMaxN; i++) {
fact[i] = mul(fact[i - 1], i);
invfact[i] = mul(invfact[i - 1], inv[i]);
}
cin >> n;
for (int i = 2; i <= n; i++) {
cin >> fa[i];
go[fa[i]].push_back(i);
}
// 获得了每个子树的拓扑数量
dfs(1);
dp[1][1] = 1;
dfs2(1);
for (int i = 1; i <= n; i++) {
cerr << g[i] << ' ';
}
cerr << endl;
for (int i = 1; i <= n; i++) {
int ans = dp[i][i];
int another = n - i - size[i] + 1;
ans = mul(ans, mul(g[i], C(another + size[i] - 1, size[i] - 1)));
cout << ans << ' ';
}
cout << endl;
return 0;
}
Problem D. Toe-Tac-Tics
不可做题,等什么时候有时间了再写。
如果看到这句话了记得评论区踢一下我去写。
Problem E. Left Shifting 3
写不出来退役吧你。
代码:
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
const std::string tmp = "nanjing";
void solve() {
int n, k;
cin >> n >> k;
std::string str;
cin >> str;
if (str.size() < 7) {
return cout << 0 << endl, void();
}
str = str + str;
std::string last;
std::vector<bool> able(str.size() + 1, false);
for (int i = 0; i <= str.size() - 7; i++) {
bool flag = true;
if (str.substr(i, 7) == tmp) {
able[i] = true;
}
}
// cout << str << endl;
int now = 0, ans = 0;
for (int i = 0; i < str.size() / 2; i++) {
if (i >= 6) now += able[i - 6];
}
for (int i = 0, j = str.size() / 2; i < str.size() / 2 && i <= k; i++, j++) {
upmax(ans, now);
now -= able[i], now += able[j - 6];
}
cout << ans << endl;
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem E. Subway
我觉得官方题解写的很不好的一个题。
在题面中,转乘有一个转出代价 \(b_i\) 和一个转入代价 \(a_i\)。但!是!题!解!写!反!了!官方题解里定义转出代价是 \(a_i\),转入代价是 \(b_i\)……我当时琢磨题解怎么看怎么不对劲,发现题解定义和题面定义相反后?
这里明确定义一下转出代价是 \(a_i\),转入代价是 \(b_i\)。
转移顺序显然不明确,考虑最短路。记一个节点为 \((i, j)\),表示在站点 \(i\)、线路 \(j\),最短路为 \(dis_{i, j}\)
转移一共分为两种:直接在当前线路上走,或者换乘到其他线路。
直接走的转移比较显然,这里讨论换乘转移:
在只考虑换乘转移的情况下,也就是从 \((i, j)\) 转移到 \((i, k)\),那么代价一定是:\(dis_{i, j} + a_i \times b_k\),显然 \(b_k\) 越小会越早入队,也就是换乘的转移一定是从 \(b_k\) 小的线路开始往上面转移的。我们发现每次查询相当于在一坨直线方程里找到某个横坐标对应的纵坐标最小值,这里可以直接写李超线段树或者维护一个动态凸包(当然我只会李超线段树)。
但是转移中有一个陷阱,就是换乘转移和普通转移会相互干扰,比较难判断究竟要不要入队下一个换乘的转移。比较简单粗暴的方法是直接建一个分层图,让换乘和普通转移独立,换乘出队后直接入队下一个位置,并且每次插入线段的时候都要松弛一下节点。
代码:
#include <set>
#include <iostream>
#include <algorithm>
#include <map>
#include <random>
#include <vector>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
const double eps = 1e-8;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
// 他妈的题解写反了
// 原题面中 a 是转入代价,但是题解里写成了转出代价
// 我真的?????你想干什么???杀人吗???
// 转移一共分为两种:一个是考虑直接走到下一个,另外一个就是换乘
int n, k, p;
int a[kMaxN], b[kMaxN];
std::map<int, std::pair<int, int>> nxt[kMaxN];
bool equal(double x, double y) {
return std::abs(x - y) <= eps;
}
bool record(int dis, int i, int k, bool flag);
class station {
std::vector<int> line;
std::vector<int> dis[2];
std::map<int, int> map;
int tot = 0;
std::vector<int> k;
std::vector<int> b;
int st;
struct node {
node* son[2];
int id;
int l, r, mid;
}* root;
int calc(int id, int x) {
if (id == 0) {
return 1e18;
}
x = ::b[line[x - 1]];
return k[id - 1] * x + b[id - 1];
}
bool better(int a, int b, int x) {
auto s1 = calc(a, x), s2 = calc(b, x);
if (s1 == s2) return a > b;
return s1 < s2;
}
void build(node* p, int l, int r) {
p->son[0] = p->son[1] = nullptr;
p->l = l, p->r = r, p->id = 0;
p->mid = (p->l + p->r) >> 1;
if (l == r) return;
int mid = (l + r) >> 1;
build(p->son[0] = new node, l, mid), build(p->son[1] = new node, mid + 1, r);
}
void insert(node* p, int l, int r, int id) {
if (p == nullptr) {
cerr << "IS NOT RIGHT" << endl;
exit(1);
}
if (l <= p->l && p->r <= r) {
if (better(p->id, id, p->l) && better(p->id, id, p->r)) return;
if (better(id, p->id, p->l) && better(id, p->id, p->r)) return p->id = id, void();
if (better(id, p->id, p->mid)) std::swap(id, p->id);
if (better(id, p->id, p->l))
insert(p->son[0], l, r, id);
else
insert(p->son[1], l, r, id);
return;
}
if (p->son[0]->r >= l) insert(p->son[0], l, r, id);
if (p->son[1]->l >= r) insert(p->son[1], l, r, id);
}
int ask(node* p, int x) {
if (p->l == p->r) return calc(p->id, x);
int ans = calc(p->id, x);
if (p->son[0]->r >= x) upmin(ans, ask(p->son[0], x));
if (p->son[1]->l <= x) upmin(ans, ask(p->son[1], x));
return ans;
}
public:
void push(int line) { this->line.push_back(line); }
void init(int kk) {
st = kk;
std::sort(line.begin(), line.end(), [](int i, int j) {
if (::b[i] == ::b[j]) return i < j;
return ::b[i] < ::b[j];
});
dis[1] = dis[0] = std::vector<int>(line.size(), 1e18);
for (int i = 0; i < line.size(); i++) {
map[line[i]] = i;
}
build(root = new node, 1, line.size());
}
// 更新最小值和在哪个线路,此时要同步维护李超线段树
void upd(int dis, int k, bool flag) {
this->dis[flag][k] = dis;
if (flag) return;
this->k.push_back(a[line[k]]);
this->b.push_back(dis);
insert(root, 1, line.size(), this->k.size());
}
// 所有查询基于下标而不是线路,减少映射的消耗
const std::vector<int>& Line() { return line; }
int id(int k) { return map[k]; }
int Dis2(int k) {
if (k + 1 > line.size()) return 1e18;
return ask(root, k + 1);
}
int Dis(int k, bool flag) {
if (k >= dis[flag].size()) return 1e18;
return dis[flag][k];
}
} s[kMaxN];
// 对应:最小值,站点和线路(下标)
std::set<std::tuple<int, int, int, bool>> set;
int cnt[kMaxN];
bool record(int dis, int i, int k, bool flag) {
if (dis >= s[i].Dis(k, flag)) {
return false;
;
}
set.erase({s[i].Dis(k, flag), i, k, flag});
s[i].upd(dis, k, flag);
set.insert({dis, i, k, flag});
return true;
}
int now[kMaxN];
void dijkstra() {
for (int i = 0, t = s[1].Line().size(); i < t; i++) {
record(0, 1, i, false);
}
while (!set.empty()) {
auto [dis, u, k, flag] = *set.begin();
set.erase(set.begin());
int line = s[u].Line()[k];
if (flag) {
now[u]++;
record(dis, u, k, false);
record(s[u].Dis2(k + 1), u, k + 1, true);
} else {
if (nxt[line].count(u)) {
auto [v, w] = nxt[line][u];
record(dis + w, v, s[v].id(line), false);
}
if (now[u] < s[u].Line().size()) record(s[u].Dis2(now[u]), u, now[u], true);
}
// 考虑换乘的转移,重复入队直到真的没法入队了
}
}
signed main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= k; i++) {
cin >> b[i];
}
for (int i = 1; i <= k; i++) {
cin >> a[i];
}
int a;
for (int i = 1; i <= k; i++) {
int u;
cin >> p >> u;
s[u].push(i);
for (int j = 1; j < p; j++) {
int w, v;
cin >> w >> v;
nxt[i][u] = {v, w};
u = v;
s[v].push(i);
}
}
for (int i = 1; i <= n; i++) {
s[i].init(i);
}
dijkstra();
for (int i = 2; i <= n; i++) {
int ans = 1e18;
for (int j = 0, t = s[i].Line().size(); j < t; j++) {
upmin(ans, s[i].Dis(j, false));
upmin(ans, s[i].Dis(j, true));
}
cout << ans << ' ';
}
return 0;
}
Problem G. Binary Tree
交互的本质是二分
还真是
代码细节题。由于查询上限卡的很死所以代码实现很细节,不是很好玩的一个题。
非二叉树这道题也是可写的,当然还是得限制一下数量不然是个错题(考虑菊花图)。非二叉树会让题目无法卡死那么严的上限,有点毒瘤其实。
在树上凑二分的性质,一个比较显然的想法是直接查询树的重心 \(u\),然后根据 \(u\) 的连边数量、查询邻居后分类讨论、再删除掉联通块。
题目的上限要求每次查询后节点规模严格减半,也就是一定不大于 \(\lfloor\frac{n}{2}\rfloor\)
分类讨论比较显然,这里只强调一下 \(u\) 的度数为 \(3\) 的时候注意事项:
一定要查询最大的两个联通块!虽然重心保证联通块大小可以小于等于 \(\lfloor\frac{n}{2}\rfloor\),但是查询后会出现留下某个联通快再加上 \(u\) 这种情况。如果不是查询最大的两个联通块,就可能会导致无法使规模严格减半,导致查询次数突破上限。
代码:
#include <algorithm>
#include <map>
#include <cassert>
#include <cmath>
#include <iostream>
#include <queue>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
std::vector<int> go[kMaxN];
int n;
int fa[kMaxN];
int lson[kMaxN];
int rson[kMaxN];
int dep[kMaxN];
bool vis[kMaxN];
int dis(int x, int y) {
int ans = 0;
if (dep[x] < dep[y]) std::swap(x, y);
while (dep[x] > dep[y]) x = fa[x], ans++;
if (x == y) return ans;
while (x != y) x = fa[x], y = fa[y], ans += 2;
return ans;
}
int s;
// #define myJugde
int cnt = 0;
std::map<int, std::map<int, int>> use;
int ask(int u, int v) {
// if (use.count(u) && use[u].count(v)) return use[u][v];
cnt++;
if (cnt > (int)log2(n)) {
cerr << n << endl;
exit(n);
}
#ifdef myJugde
int s1 = dis(u, s), s2 = dis(v, s);
if (s1 < s2) return 0;
if (s1 > s2) return 2;
return 1;
#else
cout << "? " << u << ' ' << v << endl;
cout.flush();
int tmp;
cin >> tmp;
return use[u][v] = tmp;
#endif
}
void ans(int u) {
#ifdef myJugde
if (u == s && cnt <= (int)log2(n)) {
cerr << "AC" << endl;
cerr << cnt << ' ' << (int)log2(n) << endl;
} else {
cerr << "WA" << endl;
exit(-1);
}
#else
cout << "! " << u << endl;
cout.flush();
#endif
}
int tot = 0, size = 0;
void makeTree() {
std::queue<int> q;
q.push(1);
for (int i = 1; i <= n; i++) lson[i] = rson[i] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
if (u == 0) continue;
lson[u] = rson[u] = 0;
int sonSize = rand(2, 2);
if (sonSize + size >= n) sonSize = n - size;
if (sonSize == 1) {
if (rand(0, 1))
lson[u] = ++tot;
else
rson[u] = ++tot;
} else if (sonSize == 2) {
lson[u] = ++tot;
rson[u] = ++tot;
}
size += sonSize;
q.push(lson[u]), q.push(rson[u]);
}
}
int sum[kMaxN];
bool del[kMaxN];
std::vector<std::pair<int, int>> son[kMaxN];
void dfs(int u, int fa) {
if (del[u]) return;
sum[u] = 1;
for (auto v : go[u]) {
if (v == fa || del[v]) continue;
dfs(v, u);
sum[u] += sum[v];
}
}
int p = 0, min = 1e9;
void find(int u, int fa, int faSize = 0) {
if (del[u]) return;
int m = faSize;
son[u].clear();
if (fa) son[u].push_back({faSize, fa});
for (auto v : go[u]) {
if (v == fa || del[v]) continue;
find(v, u, faSize + sum[u] - sum[v]);
upmax(m, sum[v]);
son[u].push_back({sum[v], v});
}
std::sort(son[u].rbegin(), son[u].rend());
// cerr << "FOR " << u << ' ' << m << endl;
if (p == 0 || m < min) {
p = u, min = m;
}
}
int cnt2 = 0;
void DEL(int u, int fa) {
if (del[u]) return;
del[u] = true;
sum[u] = 0;
son[u].clear();
cnt2++;
// cerr << "DEL " << u << endl;
for (auto v : go[u]) {
if (v == fa || del[v]) continue;
// if (v == fa || del[v]) continue;
DEL(v, u);
}
}
void solve() {
#ifdef myJugde
do {
n = 7;
// n = rand(2, 100);
tot = size = 1;
makeTree();
s = rand(1, n);
} while (size < n || n == 1);
cerr << "BEGIN WITH ANS " << s << ' ' << n << endl;
cerr << "MAX QUERY " << (int)log2(n) << endl;
#else
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> lson[i] >> rson[i];
}
#endif
cnt = 0;
// use.clear();
for (int i = 1; i <= n; i++) {
sum[i] = 0;
son[i].clear();
go[i].clear();
del[i] = false;
fa[i] = 0;
}
for (int i = 1; i <= n; i++) {
if (lson[i]) fa[lson[i]] = i;
if (rson[i]) fa[rson[i]] = i;
dep[i] = dep[fa[i]] + 1;
}
for (int i = 1; i <= n; i++) {
if (lson[i]) go[lson[i]].push_back(i), go[i].push_back(lson[i]);
if (rson[i]) go[rson[i]].push_back(i), go[i].push_back(rson[i]);
}
int nxt = 1;
while (1) {
int root = 0;
assert(del[nxt] == false);
min = 1e9, p = 0;
root = nxt;
dfs(nxt, 0);
find(nxt, 0, 0);
int lst = cnt2;
if (son[p].size() == 0) {
ans(p);
break;
} else if (son[p].size() == 1) {
int u = son[p][0].second;
int type = ask(p, u);
if (type == 0) {
DEL(u, p);
nxt = p;
} else {
DEL(p, u);
nxt = u;
}
} else if (son[p].size() >= 2) {
int u = son[p][0].second;
int v = son[p][1].second;
int type = ask(u, v);
if (type == 1) {
DEL(u, p), DEL(v, p);
nxt = p;
} else if (type == 0) {
DEL(p, u);
nxt = u;
} else if (type == 2) {
DEL(p, v);
nxt = v;
}
}
}
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
for (int i = 0; i <= 1e5 + 1; i++) {
go[i].reserve(3);
son[i].reserve(3);
}
int t;
cin >> t;
// t = 1e8;
while (t--) solve();
return 0;
}
Problem H. Border Jump 2
不可做题这一块。
Problem I. Bingo
可做题,但是洛谷上的题解和官方题解全都有……严重问题。跳步严重,而且洛谷题解抄题解根本讲不清式子怎么推的。
我发现 \(min-max\) 的做法,很多题解其实没有讲清楚啊。
一个排列 \(p\) 唯一对应一种填数方案 \(S\)。题目中提到需要最大值的最小值,也就是考虑 \(min-max\) 冗斥。一种划分方法的贡献是:
其中 \(T\) 代表 \(S\) 的若干行或者列共同组成的集合,也就是从 \(S\) 里挑几行几列来。
然后,所有题解到这里写的非常含糊,压根没讲 \(min-max\) 冗斥是如何跳到下一步的。洛谷真的题解抄题解一个个都不过脑子的。
最后答案也就是:
容易发现:一种 \(T\) 的贡献仅取决于它的 \(max\)。若只在贡献角度考虑 \(T\) 的话,两种 \(T\) 不同当且仅当行列划分不同或者 \(max\) 不同。而一种 \(T\) 究竟出现在多少 \(S\) 里,也就是究竟提供多少次贡献,也只取决于它的格子数量和 \(max\)。
所以我们可以直接枚举 \(T\) 的种类,也就是枚举行列划分和最大值来统计答案:
其中 \(c = i\times m + j\times n - ij\),\(a\) 要先排好序,组合数会出现超过定义域的情况,直接算 \(0\) 就好。
不放将右边那一大坨丢出来写成 \(f(c)\):
化简:
可以发现求和那里可以直接做卷积。定义函数:
这样就可以得到用来做卷积的式子:
套一个 NTT 板子就写完了。
代码:
// 好像是比较难的 min-max 冗斥
#include <iostream>
#include <algorithm>
#include <random>
#include <vector>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
const int MOD = 998244353;
const int g = 3;
int invg = 0;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int inc(int x, int y) {
return (x + y >= MOD ? x + y - MOD : x + y);
}
int dec(int x, int y) {
return (x - y < 0 ? x - y + MOD : x - y);
}
int mul(int x, int y) {
return 1ll * x * y % MOD;
}
int pow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = mul(ans, x);
x = mul(x, x);
p >>= 1;
}
return ans;
}
using poly = std::vector<int>;
std::vector<int> rev;
void NTT(std::vector<int>& v, int g) {
int n = v.size();
for (int i = 0; i < n; i++) {
if (i < rev[i]) {
std::swap(v[i], v[rev[i]]);
}
}
for (int len = 2, k = 1; len <= n; len <<= 1, k <<= 1) {
int w = pow(g, (MOD - 1) / len);
for (int i = 0; i < n; i += len) {
int now = 1;
for (int j = i; j < i + k; j++) {
int tmp = mul(now, v[j + k]);
v[j + k] = dec(v[j], tmp);
v[j] = inc(v[j], tmp);
now = mul(now, w);
}
}
}
if (g != 3) {
int inv = pow(n, MOD - 2);
for (int i = 0; i < n; i++) {
v[i] = mul(v[i], inv);
}
}
}
// 要进行差卷积,所以至少要翻四倍
int t;
int n, m;
int a[kMaxN];
int fact[kMaxN];
int inv[kMaxN];
int invf[kMaxN];
int C(int n, int r) {
return mul(fact[n], mul(invf[r], invf[n - r]));
}
int F(int c) {
if (c == 0) return 0;
int ans = 0;
for (int i = c; i <= n * m; i++) {
ans = inc(ans, mul(mul(a[i], C(i - 1, c - 1)), mul(fact[c], fact[n * m - c])));
}
return ans;
}
void solve() {
cin >> n >> m;
int need = n * m;
int len = 1;
while (len < need) len <<= 1;
for (int i = 1; i <= n * m; i++) {
cin >> a[i];
}
std::sort(a + 1, a + 1 + n * m);
// 前 len 位用来留给负数
poly v1(len << 2, 0), v2(len << 2, 0), f(len << 2, 0);
int t = len << 2;
rev.resize(t);
for (int i = 0; i < t; i++) {
rev[i] = rev[i >> 1] >> 1;
if (i & 1) rev[i] |= t >> 1;
}
v1[len] = 0;
for (int i = 1; i <= n * m; i++) {
v1[i + len] = mul(a[i], fact[i - 1]);
// 被自己气笑了
// v1[i + len - 1] = mul(a[i], fact[i - 1]);
}
for (int i = 0; i <= len; i++) {
v2[i] = invf[len - i];
}
NTT(v1, g), NTT(v2, g);
for (int i = 0; i < t; i++) {
f[i] = mul(v1[i], v2[i]);
}
NTT(f, invg);
for (int i = 0; i <= n * m; i++) {
f[i] = mul(f[i + (len << 1)], mul(i, fact[n * m - i]));
}
int ans = 0;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (i == 0 && j == 0) continue;
int opt = ((i + j) & 1) ? 1 : (MOD - 1);
int tmp = mul(opt, mul(C(n, i), C(m, j)));
tmp = mul(tmp, f[i * m + j * n - i * j]);
// tmp = mul(tmp, F(i * m + j * n - i * j));
ans = inc(ans, tmp);
}
}
cout << ans << endl;
}
signed main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
fact[0] = invf[0] = 1;
invg = pow(g, MOD - 2);
for (int i = 1; i < kMaxN; i++) {
fact[i] = mul(fact[i - 1], i);
}
invf[kMaxN - 1] = pow(fact[kMaxN - 1], MOD - 2);
for (int i = kMaxN - 2; i >= 0; i--) {
invf[i] = mul(invf[i + 1], i + 1);
inv[i + 1] = mul(fact[i], invf[i + 1]);
}
cin >> t;
while (t--) solve();
return 0;
}
Problem J. Social Media
虽然是如此简单但还是讲一下思路。
每个评论分为两种类型:只需要结交一人就可以查看或者结交两人才能查看,记作一类评论和二类评论。
我们直接找能给我提供一类评论最多的两个人交好友。如果这两个人还不优秀,那一定是某个二类评论给我们了足够的贡献,这个时候直接枚举二类评论然后交那两个人朋友统计答案即可。
代码:
#include <iostream>
#include <algorithm>
#include <map>
#include <random>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, m, k;
int ans[kMaxN];
int a[kMaxN], b[kMaxN];
int id[kMaxN];
void solve() {
cin >> n >> m >> k;
int ans = 0;
std::vector<bool> count(k + 1);
std::map<std::pair<int, int>, int> count2;
std::vector<int> count3(k + 1, 0);
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
count[x] = true;
}
for (int i = 1; i <= k; i++) id[i] = i;
for (int i = 1; i <= m; i++) {
cin >> a[i] >> b[i];
if (count[b[i]]) std::swap(b[i], a[i]);
if (count[a[i]] && count[b[i]]) {
ans++;
} else {
if (count[a[i]] && !count[b[i]]) {
count3[b[i]]++;
} else if (a[i] == b[i]) {
count3[a[i]]++;
} else {
if (a[i] > b[i]) std::swap(a[i], b[i]);
count2[{a[i], b[i]}]++;
}
}
}
std::sort(id + 1, id + 1 + k, [&](int i, int j) { return count3[i] > count3[j]; });
int ans2 = 0;
if (k >= 2) {
ans2 = count3[id[1]] + count3[id[2]];
}
for (int i = 1; i <= m; i++) {
if (count[a[i]] || count[b[i]]) continue;
if (a[i] == b[i]) continue;
if (a[i] > b[i]) std::swap(a[i], b[i]);
upmax(ans2, count3[a[i]] + count3[b[i]] + count2[{a[i], b[i]}]);
}
cerr << ans << endl;
cout << ans + ans2 << endl;
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem K. Strips
一道…… \(DP\) 题,结果我一开始写贪心去了。
其实这个题的转移结构是非常奇怪的……它的转移结构,是特么一个外向树。
黑色格子显然会将题目划分为好多好多子问题。
对于一个子问题,记 \(dp_i\) 为覆盖完第 \(i\) 个红点后,尽可能让纸条往左边放后,依旧还会多出来多少。这种状态设计显然是优秀的,它可以保证纸条尽可能能放、多放。
然后记录一下转移就写完了。
代码:
#include <cassert>
#include <algorithm>
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, m, k, w;
int solve(std::vector<int>& v, int l, int r, std::vector<int>& ansList) {
if (v.empty()) return 0;
int n = v.size();
std::vector<int> ext(n, -1), lst(n);
for (int i = 0, j = 0; i < n; i++) {
while (v[i] - v[j] + 1 > k) j++;
if (j == 0 || ext[j - 1] != -1) {
int more;
if (j == 0)
more = v[j] - l - 1;
else
more = v[j] - v[j - 1] - 1 - ext[j - 1];
lst[i] = j - 1;
int& e = ext[i];
e = v[j] + k - 1 - v[i];
e -= more;
upmax(e, 0);
}
}
if (ext[n - 1] == -1 || v.back() + ext[n - 1] >= r) {
return -1;
}
for (int i = n - 1; i >= 0; i = lst[i]) {
ansList.push_back(v[i] + ext[i] - k + 1);
}
return 1;
}
int a[kMaxN], b[kMaxN];
void solve() {
int ans = 0;
std::vector<int> ansList;
cin >> n >> m >> k >> w;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int j = 1; j <= m; j++) cin >> b[j];
b[0] = 0, b[++m] = w + 1;
std::sort(a + 1, a + 1 + n);
std::sort(b + 1, b + 1 + m);
for (int i = 1, j = 1; j <= m; j++) {
std::vector<int> list;
for (; a[i] <= b[j] && i <= n; i++) {
list.push_back(a[i]);
}
int tmp = solve(list, b[j - 1], b[j], ansList);
if (tmp == -1) {
return cout << -1 << endl, void();
}
ans += tmp;
}
cout << ansList.size() << endl;
for (auto i : ansList) cout << i << ' ';
cout << endl;
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem L. \(P \oplus Q = R\)
做不了一点,过于困难了。
Problem M. Ordainer of Inexorable Judgment
我个人认为是相当简单的计算几何题。
难度至少严格小于 Birthday Gift 那道题。
可以直接将所有节点划分成一个半径为 \(d\) 的圆。可以被攻击到当且仅当和一个圆有交。
当然会出现比较麻烦的事情:圆跨越了 \(x\) 正半轴。直接旋转九十度就好。
代码实现:
#include <cmath>
#include <iostream>
#include <random>
using std::acosl;
using std::asinl;
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
const long double pi = acos(-1);
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
long double angle(long double x, long double y) {
long double d = std::sqrt(x * x + y * y);
long double angle = acosl(x / d);
if (y < 0) angle = 2 * pi - angle;
return angle;
}
int n, d;
double t;
int x[kMaxN], y[kMaxN];
void rotate() {
for (int i = 0; i <= n; i++) {
std::swap(x[i], y[i]);
x[i] = -x[i];
}
}
const long double eps = 1e-12;
bool leq(long double l, long double r) {
if (std::abs(r - l) <= eps) return true;
if (l < r) return true;
return false;
}
long double unionn(long double l, long double r, long double L, long double R) {
if (l > r) return 0;
if (leq(L, l) && leq(r, R)) return r - l;
if (leq(l, L) && leq(R, r)) return R - L;
if (leq(L, r) && leq(l, L)) return r - L;
if (leq(l, R) && leq(R, r)) return R - l;
return 0;
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> x[0] >> y[0] >> d >> t;
bool flag = false;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i];
}
while (1) {
flag = false;
for (int i = 1; i <= n; i++) {
long double len = std::sqrt(x[i] * x[i] + y[i] * y[i]);
long double a1 = angle(x[i], y[i]);
long double a2 = asinl(d / len);
if (a1 - a2 < 0 || leq(2 * pi, a1 + a2)) flag = true;
// if (x[i] > 0) flag = true;
}
if (flag) {
rotate();
} else {
break;
}
}
// 获取每个点对应圆的交点,从而获得角度上下界
long double min = 1e9;
long double max = -1e9;
for (int i = 1; i <= n; i++) {
long double len = std::sqrt(x[i] * x[i] + y[i] * y[i]);
long double a1 = angle(x[i], y[i]);
long double a2 = asinl(d / len);
upmax(max, a1 + a2);
upmin(min, a1 - a2);
}
int round = (int)(t / (2 * pi));
t -= pi * round * 2;
long double a = angle(x[0], y[0]);
long double r = t + a;
long double r2 = -1, ans = round * (max - min);
if (r > 2 * pi) {
r2 = r - 2 * pi;
r = 2 * pi;
}
for (int i = 0; i <= n; i++) {
cerr << x[i] << ' ' << y[i] << endl;
}
cerr << endl;
cerr << a << ' ' << r << ' ' << min << ' ' << max << endl;
ans += unionn(a, r, min, max);
ans += unionn(0, r2, min, max);
printf("%.12Lf", ans);
return 0;
}

浙公网安备 33010602011771号