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\) 互质的树。
根据结论:
我们可以得到 \(N = 4 \times (2 \times \sum\limits_{i = 1}^n \varphi(i) - 1)\),表示我环视一周可以看到的树。
根据题意,我们要做的就是每次选择第 \(k\) 个,然后把他杨了,然后再继续找,直到剩 \(1\) 棵。
这是经典的约瑟夫(Josephus)问题,朴素递推时间复杂度 \(\mathcal{O}(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;
}

浙公网安备 33010602011771号