[题解] 2025 ICPC 武汉邀请赛 2025 ICPC Wuhan Invitational Contest (The 3rd Universal Cup. Stage 37 Wuhan)
中文题面:wuhan-contest-zh.pdf
赛时榜单:第 50 届 ICPC 国际大学生程序设计竞赛邀请赛武汉站 - 正式赛 | Board - XCPCIO
官方题解:2025 ICPC 国际大学生程序设计竞赛 全国邀请赛(武汉)
题号按照字典序排序,鼠标悬停至题号,可快速跳转。
A
依据题意模拟,每个属性都可以分开计算。对于每条建议,我们可以缩小属性的范围,最终检查是否 l <= r 判断合法性。
\(O(n)\)。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
void solve() {
int n, Q;
cin >> n >> Q;
vector<int> a(n + 1);
vector<pair<int, int>> b(n + 1, {1, 1e9});
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
while (Q--) {
int p, l, r;
cin >> p >> l >> r;
b[p].first = max(b[p].first, l);
b[p].second = min(b[p].second, r);
}
i64 ans = 0;
for (int i = 1; i <= n; i++) {
if (b[i].first > b[i].second) {
cout << -1 << '\n';
return;
}
if (b[i].first <= a[i] && a[i] <= b[i].second) {
continue;
} else {
ans += min(abs(b[i].first - a[i]), abs(a[i] - b[i].second));
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
C
官方题解的思路。
分类讨论,可以在打草纸上画出折线分析。因为草生长的时间是固定的,所以收割的时候的移动方向可能会变,也可能不会变。
第一次收割的时候肯定都是在 1 -> n时收割的,我们不讨论这个。
从收割之后开始计算时间:
-
如果下一次草是还是在
1 -> n时收割的,那说明每次都会在1 -> n时收割。 -
如果下一次草是在
n -> 1时收割的: (1) 如果再下一次草还是保持
n -> 1,那么说明以后一直保持n -> 1。 (2) 如果再下一次是
1 -> n,说明方向一直是变换的,1 -> n、n -> 1、1 -> n、n -> 1...
这一共是三种情况。我们需要分别考虑 收割时所在的周期。
k为常数,根据 a[i]计算得来,计算细节见代码。
- 一直是
1 -> n:1, k + 1, 2k + 1....每次收割间隔的周期是一样的。 - 一直是
n -> 1:1, k, 2k, 3k....从第二次开始,都提前一个周期。 - 两种方向交替:
1, k, 2k, 3k - 1, 4k - 1, 5k - 2...间隔的周期一次长(k),一次短(k - 1)。
代码实现时,分类讨论需要注意细节。
\(O(n + m\log m)\),计数、调和级数累加。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
void solve() {
int n, m;
cin >> n >> m;
int T = 2 * n - 1;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// g1 g2 g3 用于统计
vector<int> g1(m + 10), g2(m + 10), g3(m + 10);
// 这里算 1 ~ n - 1 ,n 单独处理
for (int i = 1; i < n; i++) {
int k = a[i] / T;
// 相隔超过 m 个周期,只有第一次能割。
if (k >= m) {
continue;
}
a[i] %= T;
int f1 = a[i] / ((n - i) * 2);
int f2 = a[i] / ((i - 1) * 2 + 1);
if (f1) { // 来 => 来 => 来 => 来 => ...
// 从 来 开始,跨越了 k 个周期,还能再跨到 下个周期前
// 所以一直是 来 时收割,每 k + 1 个周期 收割一次
g1[k + 1]++;
} else if (f2) { // 来 => 回 => 回 => 回 => ...
// 从 来 开始,因为 跨越 k 个周期后,不能跨到下个周期,
// 所以转成 回 时收割。并且能保持这个收割状态,
// 这样的话,第一次 k 个周期就能收割,后面都是 k + 1 收割一次。
g2[k + 1]++;
} else { // 来 => 回 => 来 => 回 => ...
// 原理同上,
// 来 => 回 是 k 个周期, 回 => 来 是 k + 1 个周期。
g3[k + 1]++;
}
}
// 对于 a_n,跨越 a_n 个周期收割一次,跟着 g1 走
if (a[n] / T < m) {
g1[a[n] / T + 1]++;
}
// 计算答案
vector<int> ans(m + 1);
// 首先所有草 一开始就能割一次
ans[1] = n;
for (int j = 1; j <= m; j++) {
// g1,每 j 次能 割一次
for (int i = 1 + j; i <= m; i += j) {
ans[i] += g1[j];
}
// g2,从 0 开始,
for (int i = j; i <= m; i += j) {
ans[i] += g2[j];
}
// g3,交替
int fl = 1;
for (int i = j; i <= m; i += j - (fl ^= 1)) {
ans[i] += g3[j];
}
}
for (int i = 1; i <= m; i++) {
cout << ans[i] << " \n"[i == m];
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
E
官方题解的思路。
对于每个点,我们可以做到最优策略:
- 偶数度的点:每个颜色都出现两次。
- 奇数度的点:只有一个颜色出现一次,其他的颜色出现两次。
我们可以构造环来解决这个问题,把所有奇数度的点连成偶数度。然后对每个环着同一个颜色。
边取环边删边,\(O(n + m)\)。Codeforces, 296ms通过。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
const int maxn = 2e5 + 5;
vector<pair<int, int>> g[maxn];
vector<int> vis, mp, lu, del;
vector<pair<int, int>> ed;
int co = 0;
void dfs(int x, int fe) {
if (vis[x]) {
vis[ed[fe].first] = vis[ed[fe].second] = 0;
del[fe] = 1;
mp[fe] = ++co;
while (ed[lu.back()].first != x && ed[lu.back()].second != x) {
int e = lu.back();
auto [u, v] = ed[e];
vis[u] = vis[v] = 0;
lu.pop_back();
mp[e] = co;
}
vis[ed[lu.back()].first] = vis[ed[lu.back()].second] = 0;
mp[lu.back()] = co;
lu.pop_back();
} else {
lu.push_back(fe);
}
vis[x] = 1;
while (g[x].size()) {
auto [v, id] = g[x].back();
g[x].pop_back();
if (id == fe || del[id]) {
continue;
}
del[fe] = 1;
dfs(v, id);
return;
}
}
void solve() {
co = 0;
int n, m;
cin >> n >> m;
lu.clear();
vis.assign(n + 1, 0);
for (int i = 1; i <= n; i++) {
g[i].clear();
}
vector<int> in(n + 1);
ed.assign(m + 1, {0, 0});
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
ed[i] = {u, v};
g[u].emplace_back(v, i);
g[v].emplace_back(u, i);
in[v]++;
in[u]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) {
if (in[i] % 2) {
q.push(i);
}
}
int idx = m;
while (q.size()) {
int u = q.front();
q.pop();
int v = q.front();
q.pop();
g[u].emplace_back(v, ++idx);
g[v].emplace_back(u, idx);
ed.emplace_back(u, v);
}
del.assign(ed.size(), 0);
mp.assign(ed.size(), 0);
for (int x = 1; x <= n; x++) {
for (auto &[v, id] : g[x]) {
if (!del[id]) {
vis[x] = 1;
dfs(v, id);
}
}
}
for (int i = 1; i <= m; i++) {
cout << mp[i] << " \n"[i == m];
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
G
根号分治,根据每种颜色出现的次数,对每种颜色选择不同的做法,把复杂度控制在 \(O(nm \sqrt{nm})\) 内。
首先必须知道 从(i, j)到(n, m)的路径数量怎么算。一个方向可以走n - i步,另一个方向可以走m - j步。从总步数里面选一部分走 n - i,则剩下的就是 m - j。即:\(C_{n - i + m - j} ^ {n - i}\)。
两种做法:设当前计算的颜色为 o。
- 遍历 \(n*m\) 个格子,状态只能从颜色不是
o的格子转移而来。然后统计是o的格子上的状态就可以了。 - 记录下每个颜色是
o的格子的坐标,然后排序。算到第i个坐标的时候,我们要减去被 前面的格子挡住的路径数,用总路径减去。总路径是(1, 1)到(x_i, y_i)。减去 \(\sum\)(x_j, y_j)到(x_i, y_i)。要求x_j <= x_i && y_j <= y_i。
设 cnt为颜色为o的格子的数量。
显然第一种适合在 cnt大的情况下使用。极端情况,\(cnt = \sqrt{nm}\) 时,每次\(O(nm)\),一共\(\sqrt{nm}\)次,\(O(nm\sqrt{nm})\)。
第二种在 cnt小的情况下使用,\(cnt = \sqrt{nm}\),也是 \(O(nm\sqrt{nm})\)。
所以根据cnt大小选择做法。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 2e5 + 5;
const int mod = 998244353;
i64 f[maxn], g[maxn];
i64 ksm(i64 a, i64 n) {
i64 ans = 1;
a %= mod;
while (n) {
if (n & 1) {
ans = ans * a % mod;
}
a = a * a % mod;
n >>= 1;
}
return ans;
}
void init() {
f[0] = 1;
g[0] = 1;
for (int i = 1; i < maxn; i++) {
f[i] = f[i - 1] * i % mod;
g[i] = g[i - 1] * ksm(i, mod - 2) % mod;
}
}
inline i64 C(i64 n, i64 m) {
if (m > n || n < 0 || m < 0) {
return 0;
}
if (m == 0) {
return 1;
}
return f[n] * g[m] % mod * g[n - m] % mod;
}
vector<pair<int, int>> coo[maxn];
void solve() {
int n, m;
cin >> n >> m;
vector<vector<int>> a(n + 1, vector<int>(m + 1));
vector<int> cnt(n * m + 1);
int B = sqrtl(n * m);
for (int i = 1; i <= n * m; i++) {
// coo[i].swap(vector<pair<int, int>>());
vector<pair<int, int>>().swap(coo[i]);
}
// vector<vector<pair<int, int>>> coo(n * m + 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
cnt[a[i][j]]++;
if (cnt[a[i][j]] <= B) {
coo[a[i][j]].emplace_back(i, j);
}
}
}
i64 ans = 0;
for (int o = 1; o <= n * m; o++) {
if (cnt[o] > B) {
vector<vector<i64>> dp(n + 1, vector<i64>(m + 1));
dp[1][0] = 1;
i64 res = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = (dp[i - 1][j] * (a[i - 1][j] != o) +
dp[i][j - 1] * (a[i][j - 1] != o)) % mod;
if (a[i][j] == o) {
res = (res + dp[i][j] * C(n - i + m - j, n - i)) % mod;
}
}
}
ans = (ans + res) % mod;
} else {
auto &v = coo[o];
sort(v.begin(), v.end());
vector<i64> dp(v.size());
for (int i = 0; i < v.size(); i++) {
dp[i] = C(v[i].first + v[i].second - 2, v[i].first - 1);
for (int j = 0; j < i; j++) {
if (v[j].first <= v[i].first && v[j].second <= v[i].second) {
dp[i] = (dp[i] - dp[j] * C(
v[i].first - v[j].first + v[i].second - v[j].second,
v[i].first - v[j].first
) % mod + mod) % mod;
}
}
}
i64 res = 0;
for (int i = 0; i < dp.size(); i++) {
res = (res + dp[i] * C(
n - v[i].first + m - v[i].second,
n - v[i].first
)) % mod;
}
ans = (ans + res) % mod;
}
}
cout << ans << '\n';
}
int main() {
init();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
I
一种容易想到的构造方式是,把大数往对角线里面填,最右下角不填,然后最右边一列和最下边一行尽可能的小,然后控制最右下角的数以控制答案。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
void solve() {
int n, k;
cin >> n >> k;
if (k < n || k > n * n - n + 1) {
cout << "No\n";
return;
}
cout << "Yes\n";
vector<int> vis(n * n + 1);
vector<vector<int>> a(n + 1, vector<int>(n + 1));
for (int i = 1; i <= n - 1; i++) {
int v = n * n - i + 1;
a[i][i] = v;
vis[v] = 1;
}
a[n][n] = k;
vis[k] = 1;
int cur = n * n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i][j]) {
continue;
}
while (vis[cur]) {
cur--;
}
a[i][j] = cur;
vis[cur] = 1;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cout << a[i][j] << " \n"[j == n];
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
J
显然使用后缀数组解决,对于选定的 [l_i, r_i],我们根据l_i寻找这个后缀的 rk,在rk[l_i]两侧二分,找到一个最大的区间,包含l_i并且 LCP大于r_i - l_i + 1。
区间内的每个后缀,贡献是 n - sa[i] + 1 - ht[i],n - sa[i] + 1是原本后缀的长度,减去 ht[i],重复的部分。
对于区间最开头的那个后缀,贡献是 max(0, len - 1 - ht[区间左端点]),就是把长度 >=给定的缀长度的部分都拿掉,但是可能会有残留。
对于区间的其他部分(区间左端点,区间右端点],贡献全部拿走,并清零。
我们可以用线段树维护这一操作,另外修改的时候注意一下,没有负贡献。
\(O((n + q)\log n)\)。处理SA -> ST表预处理LCP->计算贡献线段树->对于每次查询二分出最大区间->区间修改,单点修改
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
const int maxn = 2e5 + 5;
// n 字符串长度,m 桶大小
int n, m;
// od[] 作为辅助数组,作为 oldsa[] / oldrk[]
// ct[] 计数,用于排序
int od[2 * maxn], ct[maxn];
int rk[2 * maxn], sa[maxn], ht[maxn];
void sor(int k) {
for (int i = 0; i <= m; i++) ct[i] = 0;
for (int i = 1; i <= n; i++) od[i] = sa[i];
for (int i = 1; i <= n; i++) ct[rk[od[i] + k]]++;
for (int i = 1; i <= m; i++) ct[i] += ct[i - 1];
for (int i = n; i >= 1; i--) sa[ct[rk[od[i] + k]]--] = od[i];
}
// 1-indexed string, max ascii in string
void initSA(string &s, int _m) {
m = _m;
// 按第一个字母排序
for (int i = 1; i <= n; i++) rk[i] = s[i];
for (int i = n + 1; i <= 2 * n; i++) rk[i] = od[i] = 0;
for (int i = 1; i <= n; i++) sa[i] = i;
sor(0);
// 倍增
for (int k = 1; k <= n; k *= 2) {
// 第二关键字排序
sor(k);
// 第一关键字排序
sor(0);
// 把已经能分辨出的 放进不同的桶
for (int i = 1; i <= n; i++) od[i] = rk[i];
m = 0;
for (int i = 1; i <= n; i++) {
// 如果分辨不出和前一个的差距,
// 即 这一位 和 后k位 都一样,就和前一个放在一起。
if (od[sa[i]] == od[sa[i - 1]] && od[sa[i] + k] == od[sa[i - 1] + k]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++m;
}
}
// 全部都能分开,已排好。
if (m == n) break;
}
// 计算 ht[i] 代表 lcp(sa[i - 1], sa[i])
for (int i = 1, k = 0; i <= n; i++) {
// 第一名 ht 是 0
if (rk[i] == 1) continue;
// 如果上一个 ht 不是 0,按照引理这里可能会倒退 1
if (k) k--;
// 计算 lcp(i, j),和 前一个计算,用 rk[i] - 1 找前一个
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
ht[rk[i]] = k;
}
}
struct SparseTable {
int mi[maxn][25], lg[maxn];
int a[maxn];
void build() {
lg[0] = -1;
for (int i = 1; i <= n; i++) {
mi[i][0] = a[i];
lg[i] = lg[i / 2] + 1;
}
for (int i = 1; i <= lg[n]; i++) {
for (int j = 1; j + (1 << i) - 1 <= n; j++) {
mi[j][i] = min(mi[j][i - 1], mi[j + (1 << (i - 1))][i - 1]);
}
}
}
int query(int l, int r) {
int len = lg[r - l + 1];
return min(mi[l][len], mi[r - (1 << len) + 1][len]);
}
} ST;
inline int LCP(int l, int r) {
return ST.query(l + 1, r);
}
// 答案
i64 res = 0;
struct SegTree {
struct Node {
int l, r, laz;
i64 v;
} t[maxn * 4];
int a[maxn];
inline void pushup(int p) {
auto &me = t[p];
auto &lc = t[p << 1];
auto &rc = t[p << 1 | 1];
me.v = lc.v + rc.v;
}
inline void pushdown(int p) {
auto &me = t[p];
auto &lc = t[p << 1];
auto &rc = t[p << 1 | 1];
if (me.laz) {
lc.v = 0;
rc.v = 0;
lc.laz = 1;
rc.laz = 1;
me.laz = 0;
}
}
void build(int p, int l, int r) {
auto &me = t[p];
me.l = l, me.r = r;
me.laz = 0;
if (l == r) {
me.v = a[l];
return;
}
int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
pushup(p);
}
void modify(int p, int l, int r) {
auto &me = t[p];
if (l <= me.l && me.r <= r) {
res += me.v;
me.v = 0;
me.laz = 1;
return;
}
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
if (l <= mid)
modify(p << 1, l, r);
if (r > mid)
modify(p << 1 | 1, l, r);
pushup(p);
}
void remain(int p, int i, i64 d) {
auto &me = t[p];
if (i <= me.l && me.r <= i) {
res += max(0LL, me.v - d);
me.v = min(me.v, d);
return;
}
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
if (i <= mid)
remain(p << 1, i, d);
if (i > mid)
remain(p << 1 | 1, i, d);
pushup(p);
}
} T;
void solve() {
res = 0;
string s;
cin >> s;
n = s.size();
s = " " + s;
// 处理出后缀数组
initSA(s, 128);
// 快速求 LCP,基于 ST 表
for (int i = 1; i <= n; i++) {
ST.a[i] = ht[i];
}
ST.build();
// 按照 后缀排序 建树
for (int i = 1; i <= n; i++) {
T.a[i] = n - sa[i] + 1 - ht[i];
}
T.build(1, 1, n);
int Q;
cin >> Q;
while (Q--) {
int li, ri;
cin >> li >> ri;
int len = ri - li + 1;
// 寻找区间左端点
int l = 0, r = rk[li];
while (l + 1 < r) {
int mid = l + r >> 1;
if (LCP(mid, rk[li]) >= len) {
r = mid;
} else {
l = mid;
}
}
int L = r;
// 寻找右端点
l = rk[li], r = n + 1;
while (l + 1 < r) {
int mid = l + r >> 1;
if (LCP(rk[li], mid) >= len) {
l = mid;
} else {
r = mid;
}
}
int R = l;
// cout << "L R: " << L << ' ' << R << '\n';
// [L + 1, R] 区间全部置 0
if (L + 1 <= R) {
T.modify(1, L + 1, R);
}
int sta = len - 1 - ht[L];
sta = max(0, sta);
T.remain(1, L, sta);
cout << res << ' ';
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
L
注意到对数组排序不影响答案,但是更好计算。
注意到范围比较小,可以\(O(n^2)\)求解。
对于每个数作为中位数,从两侧往中心收l和r。因为排好序了,根据a[l] + a[r]和中位数的大小动态调整l和r并 check ,达到最优的效果。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a.begin() + 1, a.end());
int ans = 0;
for (int i = 1; i <= n; i++) {
int l = 1, r = n;
while (l <= i && i <= r) {
int h = a[l] + a[r];
if (h % 2 == 0 && h / 2 == a[i]) {
if (i - l <= r - i - 1) {
ans = max(ans, (i - l + 1) * 2);
} else {
ans = max(ans, (r - i) * 2 + 1);
}
}
if (h / 2 >= a[i]) {
r--;
} else {
l++;
}
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
M
官方题解第一个做法挺好的,讲的也很通透,这里直接写了个代码贴上了。
理解时注意外积方向。注意 asin和acos必须控制定义域,要不然会丢失精度 Rejected。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
struct Pt {
ld x, y, z;
};
Pt operator^(const Pt &a, const Pt &b) {
return {
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
};
}
ld operator*(const Pt &a, const Pt &b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
ld mo(const Pt &a) {
return sqrtl(a.x * a.x + a.y * a.y + a.z * a.z);
}
void solve() {
ld r;
Pt p, s, t;
cin >> r >> p.x >> p.y >> p.z >> s.x >> s.y >> s.z >> t.x >> t.y >> t.z;
Pt n = s ^ t;
// 由于叉乘特性 (x轴方向 ^ y轴方向 = z轴方向),
// 此时 n ^ s 指向 平面之间的区域(劣弧段),n ^ t 向外。
// 如果 p 在 s, n 构成的平面与 t, n 构成的平面之间 (劣弧段)
// 那么(充要), (n ^ s) * p > 0 ,(n ^ t) * p < 0。
if ((n ^ s) * p > 0 && (n ^ t) * p < 0) {
// 这个时候 s, n 构成的平面 与 p 的夹角 乘上半径 就是答案
ld d = fabsl(n * p / mo(n)) / mo(p);
d = max(-1.0L, d);
d = min(1.0L, d);
cout << asinl(d) * r << '\n';
} else {
// 否则就是在端点处取最小,s, t 分别与 p 取夹角,小角乘半径。
ld d = s * p / mo(s) / mo(p);
d = max(-1.0L, d);
d = min(1.0L, d);
ld j1 = acosl(d);
d = t * p / mo(t) / mo(p);
d = max(-1.0L, d);
d = min(1.0L, d);
ld j2 = acosl(d);
cout << min(j1, j2) * r << '\n';
}
}
int main() {
cout << fixed << setprecision(10);
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}

浙公网安备 33010602011771号