官方数据和标程:2022杭电多校资料包
目录
难度评价
中后段题目跨度很大的一场。
7题一百名,5题快两百名,4题四百名。至少补到6题,金牌补到8题以上。
D、F、G、K签到,A、B、C中等,E困难,H、I、J大难。
A. Link with Bracket Sequence II
DP
待补
B. Link with Running
图论
显然直接跑最短路即可得到答案1,最短路图是一个DAG,在DAG上跑最长路(拓扑排序递推DP)即可求得答案2。
然而本题边权可能为0,这意味着在dijkstra中,即使一个点
u
u
u 已被访问过,如果对边
(
v
→
u
,
w
=
0
)
(v \rightarrow u,w=0)
(v→u,w=0) 有
d
[
v
]
+
0
=
=
d
[
u
]
d[v]+0==d[u]
d[v]+0==d[u],则它也应该出现在最短路图中。由此可以发现最短路图可能存在0环,即它不是DAG。
如图,对应的图中存在0环,且该图直接等价于其最短路图。因为不是DAG,所以不能DP求最长路,如果尝试大力跑SPFA最长路会喜提TLE,dijkstra跑最长路会直接WA。因此需要先缩点成DAG,再跑拓扑排序。
注:题目保证答案存在且不为无穷大,说明如果在1到n的路径上有0环,则第二权值 p p p 也一定全为0;如果该0环不在1到n的路径上,则对答案无影响。
#include <bits/stdc++.h>
using namespace std;
#define int int64_t
constexpr int MAXN = 1e5 + 5;
using tpl = tuple<int, int, int>;
using PII = pair<int, int>;
vector<tpl> G[MAXN], T[MAXN], E[MAXN];
int n, m, d[MAXN], low[MAXN], num[MAXN], c[MAXN], in[MAXN], dfn, cnt;
bool vis[MAXN], inst[MAXN];
stack<int> st;
void dijkstra(int s = 1)
{
fill(vis + 1, vis + n + 1, 0);
fill(d + 1, d + n + 1, 1e18);
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 (auto [u, w, p] : G[v])
if (d[v] + w < d[u])
d[u] = d[v] + w, q.emplace(d[u], u);
}
for (int v = 1; v <= n; v++)
for (auto [u, w, p] : G[v])
if (d[v] + w == d[u])
T[v].emplace_back(u, w, p);
}
void tarjan(int v)
{
low[v] = num[v] = ++dfn;
st.push(v), inst[v] = 1;
for (auto [u, w, p] : T[v])
{
if (!num[u])
{
tarjan(u);
low[v] = min(low[v], low[u]);
}
else if (inst[u])
low[v] = min(low[v], num[u]);
}
if (num[v] == low[v])
{
++cnt;
int z;
do
{
z = st.top(), st.pop(), inst[z] = 0;
c[z] = cnt;
} while (v != z);
}
}
void connect()
{
for (int v = 1; v <= n; v++)
for (auto [u, w, p] : T[v])
if (c[v] != c[u])
E[c[v]].emplace_back(c[u], w, p), ++in[c[u]];
}
int topoSort()
{
vector<int> dp(cnt + 1);
queue<int> q;
for (int i = 1; i <= cnt; i++)
if (in[i] == 0)
q.push(i);
while (!q.empty())
{
int v = q.front();
q.pop();
for (auto [u, w, p] : E[v])
{
dp[u] = max(dp[u], dp[v] + p);
if (--in[u] == 0)
q.push(u);
}
}
return dp[c[n]];
}
void clear()
{
dfn = cnt = 0;
fill(num + 1, num + n + 1, 0);
for (int i = 1; i <= n; i++)
G[i].clear(), T[i].clear(), E[i].clear();
}
int32_t main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
cin >> n >> m;
while (m--)
{
int x, y, w, p;
cin >> x >> y >> w >> p;
G[x].emplace_back(y, w, p);
}
dijkstra();
for (int i = 1; i <= n; i++)
if (!num[i])
tarjan(i);
connect();
cout << d[n] << " " << topoSort() << endl;
clear();
}
return 0;
}
C. Magic
图论
比较明显的差分约束,容易想到用前缀和
a
[
i
]
a[i]
a[i] 表示前
i
i
i 个位置放的魔法原料之和,则不等式组为
{
a
[
m
i
n
(
n
,
i
+
k
−
1
)
]
−
a
[
m
a
x
(
0
,
i
−
k
)
]
≥
p
i
(
1
)
a
[
R
j
]
−
a
[
L
j
−
1
]
≤
B
j
(
2
)
a
[
i
]
−
a
[
i
−
1
]
≥
0
(
3
)
\begin{cases} a[min(n,i+k-1)]-a[max(0,i-k)] \geq p_i & (1) \\ a[R_j]-a[L_j-1] \leq B_j & (2)\\ a[i]-a[i-1] \geq 0 & (3)\\ \end{cases}
⎩
⎨
⎧a[min(n,i+k−1)]−a[max(0,i−k)]≥pia[Rj]−a[Lj−1]≤Bja[i]−a[i−1]≥0(1)(2)(3)
其中
(
1
)
(1)
(1) 代表位置
i
i
i 的魔力值要大于
p
i
p_i
pi,
(
2
)
(2)
(2) 代表第
j
j
j 个约束条件所规定的区间
[
L
j
,
R
j
]
[L_j,R_j]
[Lj,Rj] 內的原料之和不能多于
B
j
B_j
Bj,
(
3
)
(3)
(3) 是容易遗漏的条件,代表每个位置的原料数必须非负。
因为要求最小值,建出图后跑spfa最长路即可,有正环无解。
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e4 + 5;
vector<pair<int, int>> G[MAXN];
int n, k, q, cnt[MAXN], d[MAXN];
bool inq[MAXN];
bool spfa(int s = 0)
{
fill(d, d + n + 1, -1e9);
fill(inq, inq + n + 1, 0);
fill(cnt, cnt + n + 1, 0);
queue<int> q;
q.push(s);
d[s] = 0, cnt[s]++, inq[s] = 1;
while (!q.empty())
{
int v = q.front();
q.pop();
inq[v] = 0;
for (auto [u, w] : G[v])
{
if (d[u] < d[v] + w)
{
d[u] = d[v] + w;
if (!inq[u])
{
q.push(u);
if (++cnt[u] > n)
return 1;
inq[u] = 1;
}
}
}
}
return 0;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
cin >> n >> k;
for (int i = 1, p; i <= n; i++)
{
cin >> p;
G[max(0, i - k)].emplace_back(min(n, i + k - 1), p); // a[min(n, i+k-1)] - a[max(0, i-k)] >= p
}
cin >> q;
while (q--)
{
int l, r, b;
cin >> l >> r >> b;
G[r].emplace_back(l - 1, -b); // a[r] - a[l-1] <= b
}
for (int i = 1; i <= n; i++)
{
G[i - 1].emplace_back(i, 0); // a[i] - a[i-1] >= 0
G[0].emplace_back(i, 0);
}
cout << (spfa() ? -1 : d[n]) << "\n";
for (int i = 0; i <= n; i++)
G[i].clear();
}
return 0;
}
D. Link with Equilateral Triangle
构造
诈骗签到题。连 n = 3 n=3 n=3 都画不出来,直接猜无解。也许可以打表看小数据,但是复杂度也挺高的。证明可以参照官方题解。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int t;
cin >> t;
while (t--) cout << "No\n";
return 0;
}
E. Link with Level Editor II
数学
、数据结构
、双指针
注意到图很小,考虑用矩阵维护点对可达性(就是邻接矩阵),则矩阵乘即为方案数。如下图所示,
由于题目要求方案数不多于
k
k
k,对一个可行区间
[
l
,
r
]
[l,r]
[l,r],显然
r
r
r 增大时方案数非降,
l
l
l 增大时方案数非升,比较明显的双指针性质,于是枚举区间可以在
O
(
N
)
O(N)
O(N) 时间复杂度內完成。
则问题在于如何快速支持查询。因为是区间矩阵乘,自然想到线段树,则复杂度包括建树和每次区间查询,为
O
(
N
M
3
+
N
M
3
l
o
g
N
)
O(NM^3+NM^3logN)
O(NM3+NM3logN),因为还有多测,太慢而无法通过。官方题解使用了对顶栈的做法,这可以避免线段树每次都重新查询区间矩阵乘的弊端,实现了和矩阵求逆一样的效果,在
O
(
N
M
3
)
O(NM^3)
O(NM3) 时间内通过本题。然而线段树做法仍然有优化空间。
瓶颈来自于每次暴力矩阵乘,考虑到我们只关心
1
1
1 到
n
n
n 的方案数,仅使用一个单位矩阵
[
1
,
0
,
0
,
⋯
,
0
]
[1,0,0,\cdots,0]
[1,0,0,⋯,0] 右乘上线段树区间即可优化掉一个
M
M
M 的复杂度,总时间复杂度为
O
(
N
M
3
+
N
M
2
l
o
g
N
)
O(NM^3+NM^2logN)
O(NM3+NM2logN)。
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 5005, M = 21;
int n, m;
int64_t k, mat[MAXN << 2][M][M], tmp1[M][M], tmp2[M], f[M];
void mul(int64_t res[M][M], int64_t a[M][M], int64_t b[M][M])
{
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
tmp1[i][j] = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
for (int k = 1; k <= m; k++)
tmp1[i][j] += a[i][k] * b[k][j];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
res[i][j] = tmp1[i][j];
}
void mul(int64_t res[M], int64_t a[M], int64_t b[M][M])
{
for (int i = 1; i <= m; i++)
tmp2[i] = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
tmp2[i] += a[j] * b[j][i];
for (int i = 1; i <= m; i++)
res[i] = tmp2[i];
}
void build(int l, int r, int rt)
{
if (l == r)
{
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
mat[rt][i][j] = (i != j ? 0 : 1);
int e, v, u;
cin >> e;
while (e--)
cin >> v >> u, mat[rt][v][u] = 1;
return;
}
int mid = (l + r) >> 1;
build(l, mid, rt << 1);
build(mid + 1, r, rt << 1 | 1);
mul(mat[rt], mat[rt << 1], mat[rt << 1 | 1]);
}
void query(int L, int R, int l, int r, int rt)
{
if (L <= l && r <= R)
{
mul(f, f, mat[rt]);
return;
}
int mid = (l + r) >> 1;
if (L <= mid)
query(L, R, l, mid, rt << 1);
if (R > mid)
query(L, R, mid + 1, r, rt << 1 | 1);
}
bool check(int l, int r)
{
for (int i = 1; i <= m; i++)
f[i] = 0;
f[1] = 1;
query(l, r, 1, n, 1);
return f[m] <= k;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
cin >> n >> m >> k;
build(1, n, 1);
int l = 1, r = 0, ans = 0;
while (l <= n)
{
while (r < n && check(l, r + 1))
++r;
ans = max(ans, r - l + 1);
++l;
}
cout << ans << "\n";
}
return 0;
}
F. BIT Subway
数学
、模拟
实际价格直接模拟算,以为的价格计算起来有点阅读理解,但是注意到票可以任意拆开,意味着可以分段享受所有优惠而不亏损,因此求出票价和
s
s
s,答案是关于
s
s
s 的分段函数(注意区分花费与票价和,例如票价和为225时花费才200)
a
n
s
=
{
s
,
0
≤
s
≤
100
100
+
(
s
−
100
)
∗
0.5
,
100
<
s
≤
225
200
+
(
s
−
225
)
∗
0.8
,
s
>
225
ans= \begin{cases} \qquad \qquad s, & & 0 \leq s \leq 100 \\ 100+(s-100)*0.5, & & 100 < s \leq 225 \\ 200+(s-225)*0.8, & & s > 225 \end{cases}
ans=⎩
⎨
⎧s,100+(s−100)∗0.5,200+(s−225)∗0.8,0≤s≤100100<s≤225s>225
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cout << fixed << setprecision(3);
int t;
cin >> t;
while (t--)
{
int n;
cin >> n;
vector<int> a(n);
for (auto &x : a)
cin >> x;
double ans1 = accumulate(a.begin(), a.end(), 0), ans2 = 0;
if (ans1 > 100 && ans1 <= 225)
ans1 = 100 + 0.8 * (ans1 - 100);
else if (ans1 > 225)
ans1 = 200 + 0.5 * (ans1 - 225);
for (auto x : a)
{
if (ans2 < 100)
ans2 += x;
else if (ans2 >= 100 && ans2 < 200)
ans2 += x * 0.8;
else
ans2 += x * 0.5;
}
cout << ans1 << " " << ans2 << endl;
}
return 0;
}
G. Climb Stairs
贪心
、模拟
、数据结构
显然每次跳过一些怪物后,必须一直往下打败它们,再往上跳,对于多个可选跳点
r
1
,
r
2
,
⋯
,
r
i
r_1,r_2,\cdots,r_i
r1,r2,⋯,ri,选择
r
1
r_1
r1 总不劣(因为依然可以跳到已有跳点,还可能增加新的跳点)。于是问题变为每次寻找一个最小的
r
r
r。
设当前在
c
u
r
cur
cur,已访问过的最远点为
t
o
p
top
top,暴力枚举端点
t
o
p
<
c
u
r
+
i
≤
m
i
n
(
c
u
r
+
k
,
n
)
top<cur+i \leq min(cur+k,n)
top<cur+i≤min(cur+k,n),并检查其是否是可能的跳点,如图所示。
注意到每次检查失败就会进行区间加,容易想到线段树,但是贡献来自单点,区间操作很尴尬。稍作转换,只要左侧最小值大于等于右侧最小值,所有不等式即满足,区间最小值和区间加可以用线段树维护。
实际上不需要线段树,因为这里的区间加是全局加,线性方法详见代码。
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
int64_t a[MAXN], sum[MAXN];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
int n, k, cur = 0, top = 0;
int64_t atk;
cin >> n >> atk >> k;
for (int i = 1; i <= n; i++)
cin >> a[i], sum[i] = sum[i - 1] + a[i];
while (top != n)
{
bool ok = 0;
int64_t mn = 1e18;
for (int i = top - cur + 1; i <= k && cur + i <= n; i++)
{
mn = min(mn, atk - a[cur + i]);
if (mn >= 0)
{
atk += sum[cur + i] - sum[top];
top = cur + i, ++cur;
ok = 1;
break;
}
mn += a[cur + i + 1];
}
if (!ok)
break;
}
cout << (top == n ? "YES\n" : "NO\n");
}
return 0;
}
H. Fight and upgrade
计算几何
、数据结构
过难
I. Fall with Full Star
DP
、贪心
过难
J. Fall with Intersection
博弈论
、计算几何
过难
K. Link is as bear
数学
举例子直接猜测是线性基板题,即在 n n n 个数中选出若干个数,使得它们的异或和最大。详细证明见官方题解。
#include <bits/stdc++.h>
using namespace std;
int64_t p[64];
void insert(int64_t x)
{
for (int i = 60; i >= 0; i--)
{
if (!(x >> i))
continue;
if (!p[i])
{
p[i] = x;
break;
}
x ^= p[i];
}
}
int64_t getMax()
{
int64_t ans = 0;
for (int i = 60; i >= 0; i--)
ans = max(ans, ans ^ p[i]);
return ans;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
memset(p, 0, sizeof(p));
int n;
cin >> n;
int64_t x;
for (int i = 1; i <= n; i++)
cin >> x, insert(x);
cout << getMax() << "\n";
}
return 0;
}