[题解] 2021 CCPC 第15届 东北四省赛 部分题解 The 15th Chinese Northeast Collegiate Programming Contest
题目链接:codeforces.com/gym/103145
官方题解链接:[codeforces.com/gym/103145/attachments/download/20675/solution (1).pdf](https://codeforces.com/gym/103145/attachments/download/20675/solution (1).pdf)
鼠标停留到题号上 可以打开导航栏 快速定位题目
A
Solution 1
官方题解的思路没看懂,不过式子是对的,以下是笔者理解:
显然答案是由每一个可以产生贡献的行组成的。如何产生贡献?行内有 $i $ 使得 $ i \in [1, n]$ 即可产生贡献。
首先需要把 \([n + 1, n^2]\) 中的整数 全排列,因为他们的顺序对贡献无关。\((n^2-n)!\)
矩阵是任意排列的,我们可以把关注点放到 每一行是否能产生 1 点贡献 上。
对于每一个 \(i \in [1, n]\) ,要想产生贡献,需要从 比它大的数 中挑出 \(n-1\) 个比他小的,组成一行。 \(\sum C_{n^2 - i}^{n-1}\)
另外 \(i\) 可以放在这一行中的任意一格。\(n\)
所有行可以任意排列。\(n!\)
这样选出的行不会重复,并且能算到所有答案。
Code
时间复杂度 \(O(T*n\log n)\)。\(\log\) 来源于求组合数需要用到逆元。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 5000 * 5000 + 10;
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;
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;
}
g[m] = ksm(f[m], mod - 2);
g[n - m] = ksm(f[n - m], mod - 2);
return f[n] * g[m] % mod * g[n - m] % mod;
}
void solve() {
int n;
cin >> n;
i64 ans = f[n * n - n] * f[n] % mod * n % mod;
i64 res = 0;
for (int i = 1; i <= n; i++) {
res = (res + C(n * n - i, n - 1)) % mod;
}
cout << ans * res % mod << '\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;
}
Solution 2
赛时硬想,想出一个 \(O(T*n^2)\)的做法。直接交 TLE 了,赛后打表 在 Codeforces 提交 过了。
思路:把 \([1, n]\) 中的整数看做一种数,把 \([n +1, n^2]\) 中的整数看做另一种数,他们的顺序不影响贡献。 \(n!(n^2-n)!\)
逐个算出贡献为 \(i\) 的方案数,计入res数组。大贡献要减去小贡献的情况。
这是方案数,我们乘上 \(i\) 得到贡献为 \(tot[i]=res[i]*i\)。求和(取模)得到答案。
Code
时间复杂度:\(O(T*n^2)\)
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 5000 * 5000 + 10;
const int mod = 998244353;
ofstream out("out.txt");
ifstream in("in.txt");
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;
if (i <= 5000 + 5) {
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;
}
void solve() {
int n;
in >> n;
i64 ans = f[n] * f[n * n - n] % mod;
i64 pre = 0;
vector<i64> res(n + 1), tot(n + 1);
for (int i = 1; i <= n; i++) {
i64 c = 1;
for (int j = 0; j < n; j++) {
c = c * (i * n - j) % mod;
}
c = c * g[n] % mod;
for (int j = 1; j < i; j++) {
c = (c - C(i, j) * res[j] % mod + mod) % mod;
}
res[i] = c;
tot[i] = c * i % mod * C(n, i) % mod;
}
i64 T = 0;
for (int i = 1; i <= n; i++) {
T = (T + tot[i]) % mod;
}
out << ans * T % mod << ',';
}
int main() {
init();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
in >> tt;
while (tt--) {
solve();
}
return 0;
}
C
树形dp,遇到树形dp可以考虑创建dp[][],第一维代表节点序号,第二维代表状态序号。
这里我们可以分出 3 种效果不同的状态,节点被删除0、节点保留且存在其他节点与之相连1、节点保留且孤立2。
设当前节点为 x,x的子节点为v。
状态0:想要删除x节点,必须要求所有的v都是 0状态 或 1状态。乘法原理。
状态1:保留x节点并且至少有一个v与之相连,我们可以从所有情况中把v全部删除的情况排除掉。
状态2:保留x节点并且节点孤立,即把v全部删除。
这里可以优化,先算状态2再算状态1,好算一点。
Code
时间复杂度\(O(n)\)。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 1e5 + 5;
const int mod = 998244353;
vector<int> g[maxn];
vector<vector<i64>> dp;
void dfs(int x, int fa) {
for (auto &v : g[x]) {
if (v == fa) {
continue;
}
dfs(v, x);
dp[x][0] = dp[x][0] * (dp[v][0] + dp[v][1]) % mod;
dp[x][1] = dp[x][1] * (dp[v][0] + dp[v][1] + dp[v][2]) % mod;
dp[x][2] = dp[x][2] * dp[v][0] % mod;
}
dp[x][1] = (dp[x][1] - dp[x][2] + mod) % mod;
}
void solve() {
int n;
cin >> n;
dp.assign(n + 1, vector<i64>(3, 1));
for (int i = 1; i <= n; i++) {
g[i].clear();
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
// dp[1][2] 是 1 被孤立的情况,不合法。
cout << (dp[1][0] + dp[1][1]) % mod << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
D
注意到 对于每个 \(a_i\) 最多加 \(\log a_i\) 次 就 变成 \(1000..0000_2\) 的形式,此时再加 lowbit 就是 << 1,就是 * 2,这个时候我们就可以利用懒标记来维护。在此之前,我们可以暴力单点修改。
需要注意的是,暴力单点修改的时候不能取模,因为取模之后会影响 lowbit 判断。
数据结构题,实现的时候需要注意细节。
Code
时间复杂度:\(O(T*n*(\log n + \log a_i))\) 。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 1e5 + 5;
const int mod = 998244353;
struct SegTree {
struct Node {
int l, r;
i64 v, laz;
bool fl;
} t[maxn * 4];
int a[maxn];
inline i64 lb(i64 x) {
return x & -x;
}
inline bool check(i64 x) {
return x == lb(x);
}
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) % mod;
me.fl = lc.fl & rc.fl;
}
inline void pushdown(int p) {
auto &me = t[p];
auto &lc = t[p << 1];
auto &rc = t[p << 1 | 1];
if (me.laz > 1) {
lc.v = lc.v * me.laz % mod;
rc.v = rc.v * me.laz % mod;
lc.laz = lc.laz * me.laz % mod;
rc.laz = rc.laz * me.laz % mod;
me.laz = 1;
}
}
void build(int p, int l, int r) {
auto &me = t[p];
me.l = l, me.r = r;
me.fl = 0;
me.laz = 1;
if (l == r) {
me.v = a[l];
me.fl = check(me.v);
return;
}
int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
pushup(p);
}
void add(int p, int l, int r) {
auto &me = t[p];
// 下传到单点了,暴力修改。
// 这里必须要判 !me.fl,因为如果 变成 * 2 的情况,lowbit就不可信了
if (me.l == me.r && !me.fl) {
me.v += lb(me.v);
me.fl = check(me.v);
if (me.fl) {
me.v %= mod;
}
return;
}
// 没有下传到单点,但是下面的所有节点都可以直接 * 2了,使用懒标记
if (l <= me.l && me.r <= r && me.fl) {
me.v = me.v * 2 % mod;
me.laz = me.laz * 2 % mod;
return;
}
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
if (l <= mid)
add(p << 1, l, r);
if (r > mid)
add(p << 1 | 1, l, r);
pushup(p);
}
i64 query(int p, int l, int r) {
auto &me = t[p];
if (l <= me.l && me.r <= r) {
return me.v;
}
pushdown(p);
int mid = me.l + me.r >> 1;
i64 ans = 0;
if (l <= mid)
ans += query(p << 1, l, r);
if (r > mid)
ans += query(p << 1 | 1, l, r);
return ans % mod;
}
} T;
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> T.a[i];
}
T.build(1, 1, n);
int m;
cin >> m;
while (m--) {
int op, l, r;
cin >> op >> l >> r;
if (op == 1) {
T.add(1, l, r);
} else {
cout << T.query(1, l, r) << '\n';
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
E
令 \(k = 6p\),则 \(p\)、\(2p\)、\(3p\) 都是真因数,相加 \(=k\)。不存在输出为 \(-1\) 的情况。
Code
时间复杂度 \(O(T)\)。
void solve() {
i64 n;
cin >> n;
cout << 6 * n << ' ' << 3 << '\n';
cout << n << ' ' << 2 * n << ' ' << 3 * n << '\n';
}
I
依据题意模拟即可。
Code
时间复杂度 \(O(T*n)\)。
vector<int> c = {0, 7, 27, 41, 49, 63, 78, 108};
void solve() {
int n;
cin >> n;
i64 res = 0;
for (int i = 1; i <= n; i++) {
int x;
res += c[x];
}
if (res >= 120) {
res -= 50;
} else if (res >= 89) {
res -= 30;
} else if (res >= 69) {
res -= 15;
}
cout << res << '\n';
}
J
可以用罗德里格旋转公式做。
方便起见,我们把需要旋转的向量设为 \(p\),轴向量设为 \(v\) 。
我们计算出 \(p\) 的两个分量,平行于 \(v\) 的分量 \(p_{\parallel}\),和垂直于 \(v\) 的分量 \(p_{\perp}\)。
现在我们只需要旋转垂直分量就行了。如何旋转呢?我们可以做一个向量 和 \(p_{\perp }\) ,设为 \(q\) 构成正交基计算。
题目中旋转方向无关紧要,因为要计算两个方向的旋转。
不管怎么旋转,\(p_{\perp }\) 的分量都是在减小的。往其中一个方向转,\(q\) 的分量在增加,另一个方向转,\(q\) 的分量在减少。因为转的路径是圆,我们用 \(\sin\) 和 \(\cos\) 可以精准描绘。
Code
时间复杂度 \(O(T)\),浮点运算会慢一点。
#include <bits/stdc++.h>
using i64 = long long;
using ld = long double;
using namespace std;
const ld eps = 1e-9;
struct pt {
ld x, y, z;
};
pt operator+(const pt &a, const pt &b) {
return {a.x + b.x, a.y + b.y, a.z + b.z};
}
pt operator-(const pt &a, const pt &b) {
return {a.x - b.x, a.y - b.y, a.z - b.z};
}
pt operator*(const pt &a, const ld &k) {
return {a.x * k, a.y * k, a.z * k};
}
pt operator/(const pt &a, const ld &k) {
return {a.x / k, a.y / k, a.z / k};
}
ld len(const pt &a) {
return sqrtl(a.x * a.x + a.y * a.y + a.z * a.z);
}
ld dot(const pt &a, const pt &b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
// 叉积
pt prod(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
};
}
void solve() {
pt v, p;
ld r;
cin >> v.x >> v.y >> v.z >> p.x >> p.y >> p.z >> r;
// 单位化 轴,方便求解 平行分量
v = v / len(v);
// 平行分量 p1
pt p1 = v * dot(v, p);
// 垂直分量 p2
pt p2 = p - p1;
// 如果 p2 长度为 0 直接不用转了
if (len(p2) > eps) {
pt p3 = prod(v, p2);
// 调整 p3 长度,使它与 p2 同长
p3 = p3 * (len(p2) / len(p3));
// 题目给的是角度,函数需要用到弧度
r = r * 3.141592653589793 / 180.0;
ld c = cosl(r), s = sinl(r);
pt P = p1 + p2 * c + p3 * s;
pt Q = p1 + p2 * c - p3 * s;
p = (P.z > Q.z ? P : Q);
}
cout << fixed << setprecision(12);
cout << p.x << ' ' << p.y << ' ' << p.z << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
附1
队友的手推代码:[点赞]
#include<bits/stdc++.h>
#define ld long double
#define cos cosl
#define sin sinl
#define tan tanl
using namespace std;
int t;
ld a,b,c,x,y,z,r;
int main(){
cin>>t;
while(t--){
cin>>a>>b>>c>>x>>y>>z>>r;
r = r * 3.14159265 / 180.0;
ld R = sqrtl(a*a+b*b+c*c);
ld ux = a/R,uy = b/R,uz = c/R;
ld px = (cos(r)+ux*ux*(1-cos(r)))*x + (ux*uy*(1-cos(r))-uz*sin(r))*y + (ux*uz*(1-cos(r))+uy*sin(r))*z;
ld py = (uy*ux*(1-cos(r))+uz*sin(r))*x + (cos(r)+uy*uy*(1-cos(r)))*y + (uy*uz*(1-cos(r))-ux*sin(r))*z;
ld pz = (uz*ux*(1-cos(r))-uy*sin(r))*x + (uz*uy*(1-cos(r))+ux*sin(r))*y + (cos(r)+uz*uz*(1-cos(r)))*z;
ld qx = (cos(-r)+ux*ux*(1-cos(-r)))*x + (ux*uy*(1-cos(-r))-uz*sin(-r))*y + (ux*uz*(1-cos(-r))+uy*sin(-r))*z;
ld qy = (uy*ux*(1-cos(-r))+uz*sin(-r))*x + (cos(-r)+uy*uy*(1-cos(-r)))*y + (uy*uz*(1-cos(-r))-ux*sin(-r))*z;
ld qz = (uz*ux*(1-cos(-r))-uy*sin(-r))*x + (uz*uy*(1-cos(-r))+ux*sin(-r))*y + (cos(-r)+uz*uz*(1-cos(-r)))*z;
cout << fixed << setprecision(12);
if(pz > qz) cout<<px<<" "<<py<<" "<<pz<<'\n';
else cout<<qx<<" "<<qy<<" "<<qz<<"\n";
}
}
附2
可参考:罗德里格旋转公式。
K
将询问离线后 按 \(p\) 降序排序,将路径离线后 按 \(w\) 降序排序。利用并查集计算答案:当两个集合合并的时候,答案增加 两集合的大小乘积。
Code
时间复杂度 \(O(n + m\log m + Q\log Q + \min\{n,m\}\log n)\)。
说明:建立并查集、对路径排序、对询问排序、比较得到答案。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
struct dsu {
int n, cnt;
i64 res;
vector<int> fa, sz;
// Index 0 is invalid
dsu(int _n = 0) {
n = _n - 1;
cnt = n;
fa.resize(n + 1);
for (int i = 1; i <= n; i++)
fa[i] = i;
sz.resize(n + 1, 1);
res = 0;
}
int fin(int x) {
if (fa[x] == x)
return x;
return fa[x] = fin(fa[x]);
}
bool merg(int u, int v) {
u = fin(u), v = fin(v);
if (u == v)
return 0;
if (sz[u] < sz[v])
swap(u, v);
fa[v] = u;
res += 1LL * sz[u] * sz[v];
sz[u] += sz[v];
cnt--;
return 1;
}
};
struct edge {
int u, v, w;
};
struct qu {
int p, i;
};
void solve() {
int n, m, Q;
cin >> n >> m >> Q;
vector<edge> a(m + 1);
dsu d(n + 1);
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
a[i] = {u, v, w};
}
sort(a.begin() + 1, a.end(), [](const edge &a, const edge &b) {
return a.w > b.w;
});
vector<qu> q(Q + 1);
for (int i = 1; i <= Q; i++) {
cin >> q[i].p;
q[i].i = i;
}
vector<i64> ans(Q + 1);
sort(q.begin() + 1, q.end(), [](const qu &a, const qu &b) {
return a.p > b.p;
});
int cur = 1;
for (int i = 1; i <= m; i++) {
while (cur <= Q && q[cur].p > a[i].w) {
ans[q[cur].i] = d.res;
cur++;
}
d.merg(a[i].u, a[i].v);
}
while (cur <= Q) {
ans[q[cur].i] = d.res;
cur++;
}
for (int i = 1; i <= Q; i++) {
cout << ans[i] << '\n';
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
M
按照题意模拟,需要细心一点,过程见代码。
Code
时间复杂度:取 \(n\) 为字符个数,\(O(n)\)。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
map<string, char> sh;
sh["zh"] = 'v';
sh["sh"] = 'u';
sh["ch"] = 'i';
map<string, char> mp;
mp["q"] = mp["iu"] = 'q';
mp["w"] = mp["ei"] = 'w';
mp["e"] = 'e';
mp["r"] = mp["uan"] = 'r';
mp["t"] = mp["ue"] = 't';
mp["y"] = mp["un"] = 'y';
mp["u"] = mp["sh"] = 'u';
mp["i"] = mp["ch"] = 'i';
mp["o"] = mp["uo"] = 'o';
mp["p"] = mp["ie"] = 'p';
mp["a"] = 'a';
mp["s"] = mp["ong"] = mp["iong"] = 's';
mp["d"] = mp["ai"] = 'd';
mp["f"] = mp["en"] = 'f';
mp["g"] = mp["eng"] = 'g';
mp["h"] = mp["ang"] = 'h';
mp["j"] = mp["an"] = 'j';
mp["k"] = mp["uai"] = mp["ing"] = 'k';
mp["l"] = mp["uang"] = mp["iang"] = 'l';
mp["z"] = mp["ou"] = 'z';
mp["x"] = mp["ia"] = mp["ua"] = 'x';
mp["c"] = mp["ao"] = 'c';
mp["v"] = mp["zh"] = mp["ui"] = 'v';
mp["b"] = mp["in"] = 'b';
mp["n"] = mp["iao"] = 'n';
mp["m"] = mp["ian"] = 'm';
string S;
while (getline(cin, S)) {
stringstream ss;
ss << S;
string s;
while (ss >> s) {
if (s.size() == 1) {
cout << s + s << ' ';
} else if (s.size() == 2) {
cout << s << ' ';
} else if (s == "ang" || s == "eng" || s == "ong" || s == "ing") {
cout << s.front() << mp[s] << ' ';
} else if (sh.find(s.substr(0, 2)) != sh.end()) {
cout << sh[s.substr(0, 2)] << mp[s.substr(2)] << ' ';
} else {
cout << s.front() << mp[s.substr(1)] << ' ';
}
}
cout << '\n';
}
return 0;
}
Bonus
- 写 K 的时候因为没开
long long吃了两发罚时。 - 写 M 的时候因为笔者用的就是双拼,基本没看题就写了(其实细心一点更好)。
- 这把打的很红,A到最后才想出来打表做法,赛时没有通过。另外 Vjudge 上也不能提交这么长的代码(打出来的表)。
- 开局写的 D ,没想到 在超过 1e9 后 \(a_i\) 会在每次操作 \(*2\) 这个优化,最终没有做这个题。
- C 想了一个 假的 dp 想了半天,确实是树上 dp 练习较少。
- J 队友手推式子拿下了,泰强。

浙公网安备 33010602011771号