2026-1-3noip模拟测验总结

T1 算面积(matrix)

题意简述

给定 \(n \times m\) 的矩阵,处理 \(q\) 次询问,每次查询一个子矩形的权值和。

其中,每一行都是一个长度不超过 \(100\) 的序列重复循环构成的。

数据范围:\(1 \leq n,m,q \leq 10^5\)

题解

容易想到二维前缀和,于是题目转化为 \((1, 1)\)\((a, b)\) 的权值和。

注意到循环节长度的种类不会很多,因此不妨枚举循环节长度的种类。

这样做的好处在于确定种类之后,同类循环节都是全部和乘上同样次数再加相同一段的权值和。

因此,我们用链表状物维护循环节种类,然后令 \(s_{i,j,k}\) 表示前 \(i\) 行、循环节长度为 \(j\) 的行中,前 \(k\) 个的和,的和。

但这样直接做复杂度是 \(O(nl^2)\) 的,需要优化(其中 \(l\) 表示循环节种类,题中取 \(100\))。

考虑前 \(i\) 行、循环节长度为 \(j\) 这个条件,实际上均摊是 \(\mathcal{O}(n)\) 的,因此钦定循环节长度为 \(len_i\) 即可完美解决。

总时间复杂度 \(\mathcal{O}((n + q)l)\),大概是 \(\mathcal{O}(2 \times 10^7)\),可以通过。

参考代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;

int n, m, q, a[100005][105], lst[100005][105], s[100005][105];
string str;

inline void init(){
    for(int i = 1; i <= n; i++){
        for(int xhj = 1; xhj <= 100; xhj++) lst[i][xhj] = lst[i - 1][xhj];
        for(int xhj = 1; xhj <= a[i][0]; xhj++) s[i][xhj] = s[lst[i][a[i][0]]][xhj] + a[i][xhj];
        lst[i][a[i][0]] = i;
    }
    return;
}

inline int sum(int x, int y){
    int res = 0;
    for(int xhj = 1; xhj <= 100; xhj++){
        res += s[lst[x][xhj]][xhj] * (y / xhj);
        res += s[lst[x][xhj]][y % xhj];
    }
    return res;
}

#define y1 zhang_kevin
inline int query(int x1, int y1, int x2, int y2){
    return sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y1 - 1);
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);

    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        cin >> str;
        a[i][0] = str.length(), a[i][1] = str[0] - '0';
        for(int j = 2; j <= a[i][0]; j++) a[i][j] = a[i][j - 1] + str[j - 1] - '0';
    }
    init();
    cin >> q;
    while(q--){
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << query(x1, y1, x2, y2) << '\n';
    }
    return 0;
}

T2 前缀

题意简述

给定元素个数为 \(n\) 字符串集合 \(S\),你需要对满足要求的字符串集合 \(P\) 计数,要求如下:

  • \(|P| = m\)

  • \(T_i\) 表示 \(S\) 中以 \(P_i\) 为前缀的字符串组成的集合,则这些集合构成 \(S\) 的一个划分。

划分,定义为不重不漏,即所有部分并起来为 \(S\),但两两交集为空。

而且,划分的 \(m\) 个集合,不能存在空集。

现给定 \(n,m,S\),求 \(P\) 的个数,对 \(10^9 + 7\) 取模。

数据范围:\(1 \leq m \leq n \leq 2000, |S_i| \leq 200\)

题解

考虑将 \(S\) 按照字典序排序,则每个 \(P\)\(S\) 上一定管理一段连续区间。

考虑建字典树,然后在树上做 dp。

具体地,令 \(dp_{u,i}\) 表示节点 \(u\),已经用了 \(i\)\(P\),的数量。

然后就很好转移了。

分析复杂度,看起来是 \(\mathcal{O}(nm \times len)\) 的,但如果固定了前缀长度,转移量就变成了 \(\mathcal{O}(n)\) 量级。

因此,总时间复杂度为 \(\mathcal{O}(n^2)\),可以通过。

参考代码:

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("inline")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fstrict-aliasing")
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int ma = 1e9;
const int Mod = ma + 7;

int n, m;
string s[2005];
struct Trie{
    int son[26];
    int cnt = 0;
}trie[400005];
vector<int> e[400005];

inline vector<int> dfs(int u){
    if(trie[u].cnt > 0){
        vector<int> dp = {0, 1};
        return dp;
    }
    vector<int> res = {1};
    for(auto v : e[u]){
        vector<int> dpv = dfs(v), temp(res.size() + dpv.size() - 1, 0);
        for(int i = 0; i < (int)res.size(); i++){
            if(res[i]){
                for(int j = 0; j < (int)dpv.size(); j++){
                    if(dpv[j]){
                        temp[i + j] = (temp[i + j] + res[i] * dpv[j] % Mod) % Mod;
                    }
                }
            }
        }
        res.swap(temp);
    }

    if(u == 1) return res;
    vector<int> dp(max((int)res.size(), 2ll), 0);
    for(int i = 0; i < (int)res.size(); i++) dp[i] = (dp[i] + res[i]) % Mod;
    dp[1] = (dp[1] + 1) % Mod;
    return dp;
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);

    cin >> n >> m;
    int N = 0;
    for(int i = 1; i <= n; i++) cin >> s[i], N += s[i].length();
    sort(s + 1, s + 1 + n);

    int tot = 1;
    for(int i = 1; i <= n; i++){
        int cur = 1;
        for(int j = 0; j < (int)s[i].length(); j++){
            int ch = s[i][j] - 'a';
            if(!trie[cur].son[ch]) trie[cur].son[ch] = ++tot;
            cur = trie[cur].son[ch];
        }
        trie[cur].cnt++;
    }

    for(int u = 1; u <= N; u++){
        for(int ch = 0; ch < 26; ch++){
            if(trie[u].son[ch]){
                e[u].push_back(trie[u].son[ch]);
            }
        }
    }

    vector<int> ansdp = dfs(1);
    int ans = 0;
    if((int)ansdp.size() - 1 >= m) ans = ansdp[m] % Mod;
    cout << ans << '\n';
    return 0;
}

T3 快速排序

题意简述

咕咕咕。

题解

考虑区间 dp。

\(dp_{i, j, k, l}\) 表示考虑到区间 \([i, j]\),使用 \(k\) 个随机数,从 \(l\) 开始用,的最大代价。

显然,每次转移枚举排序后的位置 \(v\) 和左边使用的随机数个数 \(q\) 即可。

第二问也很简单,每次记录从哪一组 \((v, q)\) 转移过来,然后倒序 swap 即可。

参考代码:

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("inline")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fstrict-aliasing")
#include<bits/stdc++.h>
// #define int long long
using namespace std;
inline int read(){
    int x = 0, f = 1;
    char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)){
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}
inline void write(int x){
    if(x < 0) putchar('-'), x = -x;
    if(x > 9) write(x / 10);
    putchar(x % 10 + '0');
    return;
}
const int INF = 1e9 + 114514;
int n, a[55], dp[55][55][55][55];
int bestV[55][55][55][55], bestQ[55][55][55][55];
inline int dfs(int i, int j, int k, int l){
    if(i >= j) return dp[i][j][k][l] = (k == 0 ? 0 : -INF);
    if(dp[i][j][k][l] >= -INF) return dp[i][j][k][l];
    if(k < 1) return dp[i][j][k][l] = -INF;
    int x = a[l];
    if(x < i || x > j) return dp[i][j][k][l] = -INF;
    int res = -INF, x1, x2;
    for(int v = i; v <= j; v++){
        for(int q = 0; q <= k - 1; q++){
            x1 = dfs(i, v - 1, q, l + 1);
            if(x1 < 0) continue;
            x2 = dfs(v + 1, j, k - 1 - q, l + q + 1);
            if(x2 < 0) continue;
            int cur = x1 + x2 + j - i;
            if(cur > res){
                res = cur;
                bestV[i][j][k][l] = v;
                bestQ[i][j][k][l] = q;
            }
        }
    }
    return dp[i][j][k][l] = res;
}
vector<pair<int, int> > vec;
inline void Solve(int i, int j, int k, int l){
    if(i >= j) return;
    if(k == 0) return;
    int v = bestV[i][j][k][l];
    int q = bestQ[i][j][k][l];
    int x = a[l];
    vec.push_back({x, v});
    Solve(i, v - 1, q, l + 1), Solve(v + 1, j, k - 1 - q, l + q + 1);
    return;
}
signed main(){
    n = read();
    for(int i = 1; i <= n; i++) a[i] = read();
    for(int i = 0; i <= n; i++){
        for(int j = 0; j <= n; j++){
            for(int k = 0; k <= n; k++){
                for(int l = 0; l <= n; l++){
                    dp[i][j][k][l] = -INF - 1;
                }
            }
        }
    }
    int ans = -INF, best = -1;
    for(int k = 0; k <= n; k++){
        int cur = dfs(1, n, k, 1);
        if(cur > ans){
            ans = cur;
            best = k;
        }
    }
    if(ans == -INF){
        puts("No solution");
        return 0;
    }
    puts("Solution exists");
    cout << ans << '\n';
    Solve(1, n, best, 1);
    int res[55];
    for(int i = 1; i <= n; i++) res[i] = i;
    reverse(vec.begin(), vec.end());
    for(auto p : vec) swap(res[p.first], res[p.second]);
    for(int i = 1; i <= n; i++) cout << res[i] << ' ';
    return 0;
}

T4 果实摘取

题意简述

咕咕咕。

题解

考虑站在原点环视一周,则看到的一定是许多满足 \(x,y\) 互质的树。

根据结论:

\[\sum\limits_{i = 1}^n \sum\limits_{j = 1}^n [\gcd(i, j) == 1] = 2 \times \sum\limits_{i = 1}^n \varphi(i) - 1 \]

我们可以得到 \(N = 4 \times (2 \times \sum\limits_{i = 1}^n \varphi(i) - 1)\),表示我环视一周可以看到的树。

根据题意,我们要做的就是每次选择第 \(k\) 个,然后把他杨了,然后再继续找,直到剩 \(1\) 棵。

这是经典的约瑟夫(Josephus)问题,朴素递推时间复杂度 \(\mathcal{O}(N)\)

\[f_n = (f_{n - 1} + k) \bmod N \]

但这样做太慢了,因此考虑优化。

注意到当 \(N\)\(k\) 大很多的时候,需要加好久才能取模一次。因此,我们可以分块,每次连续加。

这样时间复杂度就正确了,约为 \(\mathcal{O}(k \ln N)\)

P.S. 还有一种做法,是递归处理,每次处理一整圈,时间复杂度 \(\mathcal{O}(\frac{N}{k} + k)\),好像也能过。

反正,我们在优秀的时间复杂度内得到了一个整数 \(t\),表示我们最后干掉的树是逆时针第 \(t\) 个看到的。

显然,可以简单转化处理 \(t\) 在不同象限的问题,下面只考虑 \(t\) 在第一象限的情况。

问题转化为:

求所有分子和分母都 \(\leq n\)最简真分数从小到大排成一行后,形成的序列中第 \(t\) 个是谁。

这是经典的Farey 序列问题,有很多做法。

直接对值域做实数二分容易出现精度问题,因此考虑其他做法。

由于这道题数据范围不大(Farey 序列模板题理论可以开到 \(10^7\)),因此可以使用一些较高复杂度做法。

这里介绍一个 \(\mathcal{O}(\sqrt{n} \times \log^3 n)\) 的做法:

先考虑给定一个分数 \(\frac{p}{q}\),如何求出其排名。

显然,我们需要求:\(\sum\limits_{i = 1}^n \sum\limits_{j = 1}^n[\frac{j}{i} \leq \frac{p}{q}][\gcd(i, j) == 1]\)

根据莫比乌斯反演,得到:\(\sum\limits_{d = 1}^n \mu(d) \sum\limits_{i = 1}^{\frac{n}{d}} \lfloor \frac{pi}{q} \rfloor\)

很像类欧对吧()

接下来,我们需要在 Stern-Brocot Tree 上做二分。

具体地,不妨令 \(\frac{a}{b}\)\(\frac{c}{d}\) 的平均为 \(\frac{a + c}{b + d}\),然后配合上面的 check 去做即可。

每次我们判断走左边还是右边,然后倍增处理最远到哪里即可。

这样做是三 \(\log\) 的,但事实证明 \(10^7\) 也跑得飞快。

还有双 \(\log\) 或更优做法,可以参考模板题的题解,这里就不提了(我不会

最后,使用欧拉筛预处理欧拉函数和莫比乌斯函数即可。

综上,我们得到了 \(\mathcal{O}(k \ln n + \sqrt{n} \times \log^3 n)\) 的做法,足以通过本题。

参考代码:

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("inline")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fstrict-aliasing")
#include<bits/stdc++.h>
#define int long long
using namespace std;

int n, k, notp[100005], p[100005], mu[100005], presmu[100005], phi[100005];

inline int Josephus(int n, int m){
    if(m == 1) return n;
    int x = 1, s = 0;
    while(x < n){
        int k = (x - s) / (m - 1);
        if((x - s) % (m - 1) != 0) k++;
        if(x + k > n) return s + (n - x) * m + 1;
        x += k, s = (s + m * k) % x;
    }
    return s + 1;
}

inline int floorSum(int n, int a, int b, int c){
    if(n < 0) return 0;
    if(a >= c || b >= c)
        return floorSum(n, a % c, b % c, c) + n * (n + 1) / 2 * (a / c) + (n + 1) * (b / c);
    int m = (a * n + b) / c;
    return n * m - floorSum(m - 1, c, c - b - 1, a);
}
inline int calc(int n, int a, int b){
    int m = b * n / a;
    if(n <= m) return floorSum(n, a, 0, b);
    return floorSum(m, a, 0, b) + (n - m) * n;
}

// 分块统计 primitive 点数
inline int count(int b, int a){
    int ans = 0;
    for(int i = 1, j; i <= n; i = j + 1){
        j = n / (n / i);
        ans += calc(n / i, b, a) * (presmu[j] - presmu[i - 1]);
    }
    return ans;
}

int ansx, ansy;
inline void Solve(int t){ //在 Farey 上找第 t 个 primitive 向量
    int x1 = 0, y1 = 1, x2 = 1, y2 = 0;
    while(true){
        int xmid = x1 + x2, ymid = y1 + y2;
        if(xmid > n || ymid > n) break;
        if(count(xmid, ymid) <= t){
            int c = 1;
            while(true){
                int xx = xmid + c * x2, yy = ymid + c * y2;
                if(xx > n || yy > n || count(xx, yy) > t) break;
                c <<= 1;
            }
            while(c){
                int xx = xmid + c * x2, yy = ymid + c * y2;
                if(xx <= n && yy <= n && count(xx, yy) <= t) xmid = xx, ymid = yy;
                c >>= 1;
            }
            x1 = ansx = xmid, y1 = ansy = ymid;
        }else{
            int c = 1;
            while(true){
                int xx = xmid + c * x1, yy = ymid + c * y1;
                if(xx > n || yy > n || count(xx, yy) <= t) break;
                c <<= 1;
            }
            while(c){
                int xx = xmid + c * x1, yy = ymid + c * y1;
                if(xx <= n && yy <= n && count(xx, yy) > t) xmid = xx, ymid = yy;
                c >>= 1;
            }
            x2 = xmid, y2 = ymid;
        }
    }
    int c = min(n / ansx, n / ansy);
    ansx *= c, ansy *= c;
    swap(ansx, ansy);
    return;
}

signed main(){
    phi[1] = mu[1] = 1;
    for(int i = 2; i <= 100000; i++){
        if(!notp[i]) p[++p[0]] = i, mu[i] = -1, phi[i] = i - 1;
        for(int j = 1; i * p[j] <= 100000; j++){
            notp[i * p[j]] = 1;
            if(i % p[j] == 0){
                mu[i * p[j]] = 0;
                phi[i * p[j]] = phi[i] * p[j];
                break;
            }
            mu[i * p[j]] = -mu[i];
            phi[i * p[j]] = phi[i] * phi[p[j]];
        }
    }
    for(int i = 1; i <= 100000; i++) presmu[i] = presmu[i - 1] + mu[i];

    cin >> n >> k;
    int sum = 0;
    for(int i = 1; i <= n; i++) sum += 2 * phi[i];
    int t = Josephus(sum << 2, k);

    if(t <= sum){ // 第一象限
        if(t == 1) ansx = n, ansy = 0; 
        else Solve(t - 1);
    }else if(t <= 2 * sum){ // 第二象限
        if(t == sum + 1) ansx = 0, ansy = n;
        else Solve(2 * sum - t + 1), ansx = -ansx;
    }else if(t <= 3 * sum){ // 第三象限
        if(t == 2 * sum + 1) ansx = -n, ansy = 0;
        else Solve(t - 2 * sum - 1), ansx = -ansx, ansy = -ansy;
    }else{ // 第四象限
        if(t == 3 * sum + 1) ansx = 0, ansy = -n;
        else Solve(4 * sum - t + 1), ansy = -ansy;
    }

    cout << ansx << ' ' << ansy << '\n';
    return 0;
}
posted @ 2026-01-04 13:15  zhang_kevin  阅读(4)  评论(0)    收藏  举报