官方数据和标程:2022杭电多校资料包
目录
难度评价
5题一百名,4题两百名,3题慢四百名。至少补到5、6题,金牌补到8题以上。
C、J、L签到,D、F、G中等,A、B、H困难,I、K大难,E没用。
A. Pandaemonium Asphodelos: The First Circle (Savage)
数据结构
要求支持4种操作:
- 将离 x x x 最近的 2 c 2c 2c 个砖块赋予一个新属性,属性序号可直接记作当前操作ID。
- 将 x x x 的属性赋到与 y y y 相连且与其属性相同的最长连续段上。
- x x x 对应的属性的权值增加 v v v。
- 查询 x x x 的权值。
考虑分别维护砖块的属性和权值。
属性需要区间推平操作,容易想到珂朵莉树(ODT),因为本题复杂度来源全是assign,效率有保证;权值需要单点查询和区间加,考虑线段树,由于
n
n
n 规模较大,必须动态开点。
具体地,
对于操作1,找到对应段,直接assign即可。这里“最近”的定义就是一维意义上的,且输入保证有
2
c
+
1
≤
n
2c+1 \leq n
2c+1≤n,即段长一定是
2
c
+
1
2c+1
2c+1,只是中心点不一定是
x
x
x。
对于操作2,先查找
x
x
x 的属性,再找到
y
y
y 所在的段,assign即可。这要求我们在维护过程中总是保证每段是最大连续段,因此assign实现中需要检查合并相邻的相同属性段。
对于操作3,找到
x
x
x 的属性,因为可能有很多零散的段,因此用一个数组
a
d
d
[
N
]
add[N]
add[N] 表示每种属性权值增量,直接加到数组上即可。因为使用了这种增量的形式维护权值,当一段的属性改变时,该段所有砖块权值要加上
a
d
d
[
a
t
t
r
l
a
s
t
]
−
a
d
d
[
a
t
t
r
n
o
w
]
add[attr_{last}]-add[attr_{now}]
add[attrlast]−add[attrnow],这是一个线段树区间加。
对于操作4,直接线段树单点查询。
#include <bits/stdc++.h>
using namespace std;
#define ls tr[rt].lc
#define rs tr[rt].rc
constexpr int MAXN = 1e5 + 5;
struct SegNode
{
int lc, rc;
int64_t val, tag;
} tr[MAXN << 5];
int n, q, tot;
int64_t add[MAXN];
inline int newNode()
{
++tot;
tr[tot].lc = tr[tot].rc = tr[tot].val = tr[tot].tag = 0;
return tot;
}
void pushDown(int rt)
{
if (tr[rt].tag)
{
if (!ls)
ls = newNode();
if (!rs)
rs = newNode();
tr[ls].val += tr[rt].tag, tr[ls].tag += tr[rt].tag;
tr[rs].val += tr[rt].tag, tr[rs].tag += tr[rt].tag;
tr[rt].tag = 0;
}
}
void modify(int rt, int L, int R, int64_t C, int l, int r)
{
if (L <= l && r <= R)
{
tr[rt].val += C, tr[rt].tag += C;
return;
}
int mid = (l + r) >> 1;
pushDown(rt);
if (L <= mid)
{
if (!ls)
ls = newNode();
modify(ls, L, R, C, l, mid);
}
if (R > mid)
{
if (!rs)
rs = newNode();
modify(rs, L, R, C, mid + 1, r);
}
}
int64_t query(int rt, int L, int l, int r)
{
if (rt == 0)
return 0;
if (l == r)
return tr[rt].val;
int mid = (l + r) >> 1;
pushDown(rt);
if (L <= mid)
return query(ls, L, l, mid);
else
return query(rs, L, mid + 1, r);
}
struct Node
{
int l, r;
mutable int v;
Node(int l, int r = 0, int v = 0) : l(l), r(r), v(v) {}
bool operator<(const Node &rhs) const { return l < rhs.l; }
};
set<Node> s;
set<Node>::iterator split(int pos)
{
auto it = s.lower_bound({pos});
if (it != s.end() && it->l == pos)
return it;
--it;
if (it->r < pos)
return s.end();
int l = it->l, r = it->r, v = it->v;
s.erase(it);
s.emplace(l, pos - 1, v);
return s.emplace(pos, r, v).first;
}
void assign(int l, int r, int v)
{
auto R = split(r + 1), L = split(l);
while (L != s.begin() && prev(L)->v == v)
--L, l = L->l;
while (R != s.end() && R->v == v)
r = R->r, ++R;
for (auto it = L; it != R; it++)
if (add[it->v] != add[v])
modify(1, it->l, it->r, add[it->v] - add[v], 1, n);
s.erase(L, R);
s.emplace(l, r, v);
}
set<Node>::iterator find(int pos) { return --s.upper_bound({pos}); }
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
int lst = 0;
cin >> n >> q;
s.clear(), s.emplace(1, n, 0);
tot = 1, tr[1].lc = tr[1].rc = tr[1].tag = tr[1].val = 0;
fill(add, add + q + 1, 0);
for (int i = 1; i <= q; i++)
{
int op, x, y;
cin >> op >> x, x = ((x - 1) ^ lst) % n + 1;
if (op == 1)
{
cin >> y, y = ((y - 1) ^ lst) % ((n - 1) / 2) + 1;
int l = x - y, r = x + y;
if (l < 1)
r += 1 - l, l += 1 - l;
else if (r > n)
l -= r - n, r -= r - n;
assign(l, r, i);
}
else if (op == 2)
{
cin >> y, y = ((y - 1) ^ lst) % n + 1;
auto it1 = find(x), it2 = find(y);
if (it1->v != it2->v)
assign(it2->l, it2->r, it1->v);
}
else if (op == 3)
{
cin >> y, add[find(x)->v] += y;
}
else
{
auto ans = query(1, x, 1, n) + add[find(x)->v];
cout << ans << "\n";
lst = ans & 1073741823;
}
}
}
return 0;
}
B. Jo loves counting
数学
、暴力
过难
C. Slipper
图论
明显要跑最短路,但是建边上不能过于暴力,考虑每层新增一个中转点,它到本层点的边权为 0 0 0,与其相差 k k k 层的结点都可以跳转到它且边权为 p p p。
注:机子太慢了,不用前向星容易被卡常。
#include <bits/stdc++.h>
using namespace std;
#define int int64_t
constexpr int MAXN = 2e6 + 5, MAXM = 2e7;
using PII = pair<int, int>;
int head[MAXN], tot = 1;
int d[MAXN], dep[MAXN], mxdep;
bool vis[MAXN];
struct Edge
{
int to, next, w;
} e[MAXM];
void add(int u, int v, int w)
{
e[++tot].to = v, e[tot].w = w, e[tot].next = head[u];
head[u] = tot;
}
void clear(int n)
{
for (int i = 1; i <= n + mxdep; i++)
head[i] = 0;
tot = 1;
}
void dfs(int v, int fa)
{
dep[v] = dep[fa] + 1;
mxdep = max(mxdep, dep[v]);
for (int i = head[v]; i; i = e[i].next)
if (e[i].to != fa)
dfs(e[i].to, v);
}
void dijkstra(int s)
{
memset(vis, 0, sizeof(vis));
memset(d, 0x3f, sizeof(d));
d[s] = 0;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.emplace(0, s);
while (!q.empty())
{
auto [_, v] = q.top();
q.pop();
if (vis[v])
continue;
vis[v] = 1;
for (int i = head[v]; i; i = e[i].next)
{
int u = e[i].to, w = e[i].w;
if (!vis[u] && d[v] + w < d[u])
d[u] = d[v] + w, q.emplace(d[u], u);
}
}
}
int32_t main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
{
int n, k, p, s, t;
cin >> n;
for (int i = 1; i < n; i++)
{
int v, u, w;
cin >> v >> u >> w, add(v, u, w), add(u, v, w);
}
cin >> k >> p >> s >> t;
mxdep = 0;
dfs(1, 0);
for (int i = 1; i <= n; i++)
{
add(n + dep[i], i, 0);
if (dep[i] - k >= 1)
add(i, n + dep[i] - k, p);
if (dep[i] + k <= mxdep)
add(i, n + dep[i] + k, p);
}
dijkstra(s);
cout << d[t] << "\n";
clear(n);
}
return 0;
}
D. The Surveying
计算几何
数据范围已经给出了重要提示,那么实际上这题就做完了。容易想到 O ( 2 N ) O(2^N) O(2N) 状压枚举控制点,检查是否能看到所有Detail Points且控制点间连通,考虑用bitset优化一下,这部分复杂度为 O ( 2 N ⋅ N ( N + M ) w ) O(2^{N} \cdot \cfrac{N(N+M)}{w}) O(2N⋅wN(N+M))。预处理时对每个控制点,暴力求出其能看到的Detail Points和控制点,复杂度 O ( N M 2 + N 2 M ) O(NM^2+N^2M) O(NM2+N2M),做法为判断线段相交。控制点之间的线段不应该与任何矩形边相交;控制点和Detail Point之间的线段如果与矩形边相交,交点必须是该Detail Point。控制点在矩形边界延长线上的情况可能会带来一些疑惑,实际上不需要特判。
#include <bits/stdc++.h>
using namespace std;
struct Point
{
int64_t x, y;
Point() {}
Point(int64_t x, int64_t y) : x(x), y(y) {}
bool operator!=(Point B) { return x != B.x || y != B.y; }
Point operator-(Point B) { return Point(x - B.x, y - B.y); }
} ctrl[20], detail[400];
using Vector = Point;
int64_t Cross(Vector A, Vector B) { return A.x * B.y - A.y * B.x; }
bool Cross_segment(Point a, Point b, Point c, Point d)
{
__int128_t c1 = Cross(b - a, c - a), c2 = Cross(b - a, d - a);
__int128_t d1 = Cross(d - c, a - c), d2 = Cross(d - c, b - c);
return c1 * c2 < 0 && d1 * d2 < 0; // 这里爆ll了,看了很久
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
int n, m;
cin >> n >> m;
vector<bitset<400>> bs1(n);
vector<bitset<20>> bs2(n);
for (int i = 0; i < n; i++)
cin >> ctrl[i].x >> ctrl[i].y;
for (int i = 0; i < m * 4; i++)
cin >> detail[i].x >> detail[i].y;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
bool flg = 0;
for (int k = 0; k < m && !flg; k++)
for (int p = 0; p < 4; p++)
flg |= Cross_segment(ctrl[i], ctrl[j], detail[k * 4 + p], detail[k * 4 + (p + 1) % 4]);
if (!flg)
bs2[i][j] = bs2[j][i] = 1;
}
for (int j = 0; j < m * 4; j++)
{
bool flg = 0;
for (int k = 0; k < m && !flg; k++)
for (int p = 0; p < 4; p++)
{
auto p1 = detail[k * 4 + p], p2 = detail[k * 4 + (p + 1) % 4];
flg |= p1 != detail[j] && p2 != detail[j] && Cross_segment(ctrl[i], detail[j], p1, p2);
}
if (!flg)
bs1[i][j] = 1;
}
}
int ans = 2568;
for (int i = 1; i < (1 << n); i++)
{
bitset<400> tmp1;
bitset<20> tmp2;
for (int j = 0; j < n; j++)
if (i & (1 << j))
tmp1 |= bs1[j], tmp2 |= bs2[j];
tmp2 &= i, tmp2 ^= i;
if (tmp1.count() == m * 4 && tmp2.count() == 0)
ans = min(ans, __builtin_popcount(i));
}
if (ans == 2568)
cout << "No Solution!\n";
else
cout << ans << "\n";
}
return 0;
}
E. 3D Puzzles
Dancing Links X(舞蹈链)
没用
F. BBQ
DP
给队友了
G. Count Set
数学
、图论
给队友了
H. AC/DC
字符串
、数据结构
待补
I. Cube Rotate
数学
过难
J. Bragging Dice
博弈论
神必签到题,题意模糊。
正确题意如下:两个人各有
n
n
n 个骰子,并且已知所有骰子上的数。游戏轮流进行,本回合玩家可以声明【场上有
x
x
x 个骰子上的数是
y
y
y】,如果之前有玩家声明过,那他只能声明【场上有
x
1
x_1
x1
(
x
1
>
x
)
(x_1>x)
(x1>x) 个骰子上的数是
y
1
y_1
y1
(
1
≤
y
1
≤
6
)
(1 \leq y_1 \leq 6)
(1≤y1≤6)】或者【场上有
x
2
x_2
x2
(
x
2
=
x
)
(x_2=x)
(x2=x) 个骰子上的数是
y
2
y_2
y2
(
y
2
>
y
)
(y_2>y)
(y2>y)】。他也可以选择质疑对手的声明,质疑成功则获胜,否则失败。有几条特殊规则,如果一个玩家的
n
n
n 个骰子点数各不相同,则无视这些骰子;如果一个玩家的
n
n
n 个骰子点数都相同,视为他还有一个同点数的骰子;否则,如果从来没有玩家声明过点数为
1
1
1 的骰子,则点数为
1
1
1 的骰子可以在声明中当任意点数使。
非公平博弈,假设所有点数最大出现次数为
m
x
mx
mx,对应的数为
x
1
,
⋯
,
x
k
x_1,\cdots,x_k
x1,⋯,xk (
x
1
<
.
.
.
<
x
k
x_1<...<x_k
x1<...<xk),如果先手直接声明【场上有
m
x
mx
mx 个骰子上的数是
x
k
x_k
xk】,则后手无法反制,因为既不能使用声明1,也不能使用声明2。
只有一种情况先手必败,即
m
x
=
0
mx=0
mx=0,因为不允许声明有
0
0
0 个骰子,此时对应情况为两个玩家各自的
n
n
n 个骰子上的数都不同,两人都符合特殊规则1,场上没有任何骰子,先手无法进行任何合法声明。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
int n, x;
cin >> n;
bool flg = 1;
for (int i = 1; i <= 2; i++)
{
int cnt[7] = {0};
for (int j = 1; j <= n; j++)
{
cin >> x;
if (++cnt[x] != 1)
flg = 0;
}
}
cout << (!flg ? "Win!\n" : "Just a game of chance.\n");
}
return 0;
}
K. Kazuha’s String
字符串
、数学
、搜索
、暴力
本质上是一个群论问题,可以模拟六面体旋转群,然而难度不小,而且对群论一无所知。
实际上也可以认为是一个图论问题,以最短的字符串为根,生成(通过任意插入和删除对应子串)的所有串都处于同一连通块,且等价于根字符串。容易猜想连通块个数不会太多。
于是直接从小到大枚举字符串,暴力搜索,可以发现只有24个连通块。
虽然找到了连通块,但是每个块中只能爆搜到很小规模的字符串,从而在询问串
s
s
s 很长时,难以直接获取
r
o
o
t
[
s
]
root[s]
root[s]。注意到根字符串长度总是
≤
4
\leq 4
≤4,这意味着所有长度
≥
5
\geq 5
≥5 的串都能转换成长度
≤
4
\leq 4
≤4 的串,那么求出长度
≤
5
\leq 5
≤5 的字符串的转换关系,即可不断取出
s
s
s 的前缀字符,并最终转换得到根字符串。
所以其实最终打完表只有72个转换关系,直接写死在程序中即可省去打表复杂度,轻松通过。
#include <bits/stdc++.h>
using namespace std;
string op[] = {"aa", "bbb", "cccc", "abababab", "acacac", "bcbc", "abc"};
map<string, string> mp;
string rt;
void dfs(const string &cur)
{
if (cur.size() > 11 || mp.count(cur))
return;
mp[cur] = rt;
for (int i = 0; i < cur.size(); i++) // 删除
for (const auto &s : op)
if (cur.substr(i, s.size()) == s)
dfs(cur.substr(0, i) + cur.substr(i + s.size()));
for (int i = 0; i <= cur.size(); i++) // 插入
for (const auto &s : op)
dfs(cur.substr(0, i) + s + cur.substr(i));
}
void bfs()
{
set<string> root;
queue<string> q;
q.push("");
while (!q.empty())
{
auto s = q.front();
q.pop();
if (mp.count(s))
continue;
root.insert(s), rt = s;
dfs(s);
q.push(s + 'a'), q.push(s + 'b'), q.push(s + 'c');
}
for (const auto &s : root)
for (char ch = 'a'; ch <= 'c'; ch++)
printf("mp[\"%s%c\"] = \"%s\";\n", s.c_str(), ch, mp[s + ch].c_str());
}
void init()
{
mp["a"] = "a";
mp["b"] = "b";
mp["c"] = "c";
mp["aa"] = "";
mp["ab"] = "ab";
mp["ac"] = "ac";
mp["aba"] = "aba";
mp["abb"] = "abb";
mp["abc"] = "";
mp["abaa"] = "ab";
mp["abab"] = "cc";
mp["abac"] = "abac";
mp["abaca"] = "ccb";
mp["abacb"] = "cbac";
mp["abacc"] = "acb";
mp["abba"] = "ac";
mp["abbb"] = "a";
mp["abbc"] = "aba";
mp["aca"] = "abb";
mp["acb"] = "acb";
mp["acc"] = "acc";
mp["acba"] = "acba";
mp["acbb"] = "acbb";
mp["acbc"] = "abb";
mp["acbaa"] = "acb";
mp["acbab"] = "abac";
mp["acbac"] = "bacb";
mp["acbba"] = "acc";
mp["acbbb"] = "ac";
mp["acbbc"] = "acba";
mp["acca"] = "acbb";
mp["accb"] = "accb";
mp["accc"] = "b";
mp["accba"] = "bac";
mp["accbb"] = "ba";
mp["accbc"] = "acbb";
mp["ba"] = "ba";
mp["bb"] = "bb";
mp["bc"] = "a";
mp["baa"] = "b";
mp["bab"] = "acc";
mp["bac"] = "bac";
mp["baca"] = "accb";
mp["bacb"] = "bacb";
mp["bacc"] = "cb";
mp["bacba"] = "cbac";
mp["bacbb"] = "cba";
mp["bacbc"] = "accb";
mp["bba"] = "c";
mp["bbb"] = "";
mp["bbc"] = "ba";
mp["ca"] = "bb";
mp["cb"] = "cb";
mp["cc"] = "cc";
mp["cba"] = "cba";
mp["cbb"] = "cbb";
mp["cbc"] = "bb";
mp["cbaa"] = "cb";
mp["cbab"] = "bac";
mp["cbac"] = "cbac";
mp["cbaca"] = "bacb";
mp["cbacb"] = "acba";
mp["cbacc"] = "ccb";
mp["cbba"] = "cc";
mp["cbbb"] = "c";
mp["cbbc"] = "cba";
mp["cca"] = "cbb";
mp["ccb"] = "ccb";
mp["ccc"] = "ab";
mp["ccba"] = "abac";
mp["ccbb"] = "aba";
mp["ccbc"] = "cbb";
}
string f(const string &s)
{
string res;
for (auto ch : s)
res = mp[res + ch];
return res;
}
int main()
{
// bfs();
init();
int T;
cin >> T;
while (T--)
{
string s, t;
cin >> s >> t;
cout << (f(s) == f(t) ? "yes\n" : "no\n");
}
return 0;
}
L. Buy Figurines
数据结构
、模拟
需要一个堆来维护每个队列的人数,为了方便实时修改可以使用set。对每个队列,另外记录其人数和最晚结束时间。将顾客按到达时间排序后,每次先把所有已经完成购买的顾客出队,因此还需要一个堆来维护当前所有顾客的结束时间,这之后再让当前顾客入队。其实只关心每个队列的最晚时间,因此出队入队不需要队列数据结构,直接维护时间即可。
#include <bits/stdc++.h>
using namespace std;
using PII = pair<int64_t, int64_t>;
constexpr int MAXN = 2e5 + 5;
PII vec[MAXN];
int64_t sz[MAXN], lst[MAXN];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
{
int64_t ans = 0;
int n, m;
cin >> n >> m;
set<PII> S, del;
for (int i = 1; i <= n; i++)
cin >> vec[i].first >> vec[i].second;
sort(vec + 1, vec + n + 1);
for (int i = 1; i <= m; i++)
S.emplace(0, i), sz[i] = lst[i] = 0;
for (int i = 1; i <= n; i++)
{
auto [a, s] = vec[i];
while (!del.empty())
{
auto [t, id] = *del.begin();
if (t > a)
break;
del.erase(del.begin());
S.erase({sz[id], id}), S.emplace(--sz[id], id);
}
auto [_, id] = *S.begin();
S.erase(S.begin()), S.emplace(++sz[id], id);
if (lst[id] <= a)
lst[id] = a + s;
else
lst[id] += s;
del.emplace(lst[id], id);
ans = max(ans, lst[id]);
}
cout << ans << "\n";
}
return 0;
}