Loading

[题解] 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!\)

这样选出的行不会重复,并且能算到所有答案。

\[ans = (n^2-n)!*\sum_{i=1}^{n}(C_{n^2 - i}^{n-1}*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数组。大贡献要减去小贡献的情况。

\[res[1] = C_n^n \]

\[res[2] = C_{2n}^n - C_2^1 * res[1] \]

\[res[3] = C_{3n}^n-C_3^1*res[2]-C_3^2*res[1] \]

\[.... \]

\[res[k] = C_{kn}^n- \sum_{i = 1}^k C_k^i \ res[i] \]

这是方案数,我们乘上 \(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

设当前节点为 xx的子节点为v

状态0:想要删除x节点,必须要求所有的v都是 0状态 或 1状态。乘法原理。

\[dp[x][0]= \prod_v (dp[v][0] + dp[v][1]) \]

状态1:保留x节点并且至少有一个v与之相连,我们可以从所有情况中把v全部删除的情况排除掉。

\[dp[x][1] = \prod_v (dp[v][0] + dp[v][1] + dp[v][2]) - \prod_v dp[v][0] \]

状态2:保留x节点并且节点孤立,即把v全部删除。

\[dp[x][2] = \prod_v dp[v][0] \]

这里可以优化,先算状态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

  1. 写 K 的时候因为没开 long long 吃了两发罚时。
  2. 写 M 的时候因为笔者用的就是双拼,基本没看题就写了(其实细心一点更好)。
  3. 这把打的很红,A到最后才想出来打表做法,赛时没有通过。另外 Vjudge 上也不能提交这么长的代码(打出来的表)。
  4. 开局写的 D ,没想到 在超过 1e9 后 \(a_i\) 会在每次操作 \(*2\) 这个优化,最终没有做这个题。
  5. C 想了一个 假的 dp 想了半天,确实是树上 dp 练习较少。
  6. J 队友手推式子拿下了,泰强。
posted @ 2025-05-11 17:13  Music163  阅读(88)  评论(0)    收藏  举报