2026年1月13日 模拟赛总结
前言
比赛链接:https://htoj.com.cn/cpp/oj/contest/detail?cid=22634204630016&gid=22459154916992
挂分:\(150\sim 200\)。
优秀的成绩:\(64\) 分。
战绩:最后一道题全场最高,排名倒数第三。
春
- 赛时因为没打
freopen挂掉了
众所周知,\(a\bmod m=a-k\times m,\sum(a_i\bmod m)=\sum(a_i-k_i\times m)=\sum{a_i}-\sum{k_i}\times m\)。也就是说,对于每一个 \(m\) 我们只要把这个 \(\sum{k_i}\) 给处理出来就可以了。
然后再想,对于超过 \(\max(a_i)\) 的 \(m\),那它肯定不需要处理,直接输出 \(\sum{a_i}\) 就可以了。
所以现在就只需要处理小于等于 \(\max(a_i)\) 的 \(m\) 了。
假设现在有一个数组 \(p\) 与一个你要处理的模数 \(m\),其中 \(p_i=\lfloor\frac{i}{m}\rfloor\),那么 \(p_i=p_{i-1} \textup{或}\ p_i=p_{i-1}+1(i>1)\),证明也是显然。
举个例子,假设模数是 \(2\),那么 \([2,3]=1,[4,5]=2\)。
很容易就可以发现 \(p_i\) 是我们要处理的 \(k_i\),下文中的 \(p_i\) 都可以理解成 \(k_i\)。
因此我们要先预处理出所有的 \(m(1\le m\le 10^5)\),然后由于我们的 \(k_i\) 对应在 \(p\) 上是连续的,因此可以很容易得出我们要求的 \(\sum{k_i}\)。
这里我们再设两个数组 \(f,s\),其中 \(f_i\) 表示 \(i\) 在 \(a\) 中出现的次数,\(s_i=s_{i-1}+f_i\),即前缀和。
之后我们又假设 \([l,r]\) 中的所有数的 \(k\) 都相等,那么它们的贡献就是 \((s_r-s_{l-1})\times k\)。
对于所有的 \(m\) 都这么去计算贡献,对于任意 \(m\) 都去找上述 \([l,r]\)。由于是调和级数,复杂度为 \(O(n\log n)\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
const int M = 1e5;
int n, Q, ans[N];
int a[N], p[N], q[N];
signed main() {
freopen("spring.in", "r", stdin);
freopen("spring.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> Q;
int sum = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i], p[a[i]]++;
sum += a[i];
}
// 前缀和计算
for (int i = 1; i <= M; i++)
q[i] = q[i - 1] + p[i];
for (int i = 1; i <= M; i++) { // 枚举模数
for (int j = i, k = 1; j <= M; j += i, k++) {
// 枚举区间 [l, r] 与对应的 k,这里 l = j - 1, r = i + j - 1
ans[i] += (q[min(i + j - 1, M)] - q[j - 1]) * k; // 计算贡献
}
}
int x;
while (Q--) {
cin >> x;
if (x > M) cout << sum << "\n"; // x > M 的情况无需处理
else cout << sum - ans[x] * x << "\n"; // 这里算出来的 ans[x] 就是对于 x 的 k_i 之和
}
return 0;
}
夏
- 赛时因为数组开到了 \(10^3\times 10^5\) 挂掉了
\(30\) 分做法:
- 状态:定义 \(f(i,j)(1\le i\le k,1\le j\le n)\) 表示长度为 \(i\) 的序列中(包含 \(i\)),最后一个数为 \(j\) 的不同序列的数量
- 状态转移方程:\(f(i,j)={\sum_{k=1}^{\min(\lfloor \frac{n}{j}\rfloor,j)}} f(i-1,k)\)
- 初始化:\(f(1,j)=1(1\le j\le n)\)
答案就是 \(\sum_{j=1}^{n} f(k,j)\)。
其中 \(\sum f(i-1,k)\) 可以被前缀和搞定,时间复杂度为 \(O(nk)\)。
\(60\) 分做法(赛时做法):
由于只有最后一个数可以是大于 \(\sqrt n\) 的数,因此我们将 \(j\) 的枚举范围改成 \(1\le j\le \sqrt n\)。
由于倒数第二个数只有可能是 \(\lfloor \frac{n}{j}\rfloor(j>\sqrt n)\),也就是说,这部分的贡献是连续的,因此我们只要枚举 \(\frac{n}{j}\) 就可以顺着得到贡献了。
时间复杂度 \(O(\sqrt nk)\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
const int K = 1e2 + 10;
const int mod = 1e9 + 7;
int n, k;
int f[K][N], g[K][N];
signed main() {
freopen("summer.in", "r", stdin);
freopen("summer.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
int sqn = (int)sqrt(n);
for (int i = 1; i <= sqn; i++)
f[1][i] = 1, g[1][i] = i;
for (int i = 2; i <= k; i++) {
for (int j = 1; j <= sqn; j++)
f[i][j] = g[i - 1][min(n / j, j)] % mod;
for (int j = 1; j <= sqn; j++)
g[i][j] = (g[i][j - 1] + f[i][j]) % mod;
}
int ans = g[k][sqn];
for (int i = 1; i <= sqn; i++) {
if (n / i <= sqn) continue;
ans = (ans + g[k - 1][i] * (n / i - n / (i + 1))) % mod;
}
cout << ans << endl;
return 0;
}
满分做法(神仙做法):
我们想要将上述 DP 加快,但是你或许会往组合数的角度来想。那就让我们来试试。
对于 \(f_{i,j}\),由于 \(j\le \sqrt n\),因此我们只要保证 \(a_i=j,a_1\le a_2\le ...\le a_i\) 就可以了,所以让我们转换一下:
由于 \(a_i\) 为正整数,因此 \(b_1\ge 1,b_2,b_3\cdots b_i\ge 0\),且 \((\sum_{j=1}^i b_j)=a_i\)。
也就是说,我们要解一个方程,即 \(b_1+b_2+b_3+\cdots+b_i=a_i(b_1\ge 1,b_2,b_3\cdots b_i\ge 0)\)。
由于插板法善于解决盒子全为空的情况,因此我们将上述方程改为 \(b_1'+b_2+b_3+\cdots+b_i=a_i-1(b_1',b_2,b_3\cdots b_i\ge 0)\),其中 \(b_1'=b_1-1\)。
这不就相当于给定 \(i\) 个盒子,\(j-1\) 个球,盒子可以为空,我们要将 \(j-1\) 个球放入 \(i\) 个盒子的方案数吗!
插板法启动:给定 \(n\) 个盒子,\(m\) 个球,盒子可为空,那么方案数为 \(C(n+m-1,n-1)=C(n+m-1,m)\)。代入可得,\(f_{i,j}=C(i+(j-1)-1,i-1)=C(i+j-2,i-1)=C(i+j-2,j-1)\)。
然后就相当于你在 \(O(\sqrt n)\) 的时间复杂度以内就求出了 \(f_{k,j}(1\le j\le \sqrt n)\),后面的计算与 \(60\) 分的一模一样。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
const int K = 1e3 + 10;
const int mod = 1e9 + 7;
int n, k, fac[N], inv[N];
// 阶乘数组与逆元数组
int pw(int a, int b) {
int sum = 1;
while (b) {
if (b & 1) sum = sum * a % mod;
a = a * a % mod;
b >>= 1;
}
return sum;
}
int C(int a, int b) {
if (a < 0 || b < 0 || a > b) return 0;
return fac[b] * inv[a] % mod * inv[b - a] % mod;
}
void init() { // 处理 fac 与 inv
fac[0] = 1;
for (int i = 1; i <= N - 5; i++) fac[i] = fac[i - 1] * i % mod;
inv[N - 5] = pw(fac[N - 5], mod - 2);
for (int i = N - 6; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
}
signed main() {
freopen("summer.in", "r", stdin);
freopen("summer.out", "w", stdout);
cin >> n >> k;
init();
int sqn = sqrt(n), ans = 0;
// 处理最后一个数 <= sqn 的情况
for (int i = 1; i <= sqn; i++)
ans = (ans + C(k - 1, k + i - 2)) % mod;
// 处理最后一个数 > sqn 的情况
for (int i = 1; i <= sqn; i++) {
int L = max(sqn + 1, n / (i + 1) + 1);
int R = n / i;
if (L > R) continue;
ans = (ans + (R - L + 1) % mod * C(k - 1, k + i - 2) % mod) % mod;
}
cout << ans << endl;
return 0;
}
秋
考虑到区间的存在意义只与它能包含的点的数量相关,因此我们的 \([l_i,r_i]\) 不再是数轴上的区间,而是对应能包含 \(a\) 中的点的区间。
举个例子:\([l,r]=[2,4],a=[1,3,4,5]\),那么 \([l,r]\) 将被换成 \([2,3]\)。
对于这种答案与序列顺序无关的题目,不如先想想排序。
由于我们最后需要使用线段树优化 DP,因此我们从 DP 的方面去思考排序方式。
- 状态:设 \(f_{i,j}\) 表示对于前 \(i\) 个区间,可以刚好覆盖 \(1\sim j\) 的方案数
- 状态转移方程:
分三种情况讨论:
- \(j<r_i\):如果选了这个区间,那么 \(j\) 后面的点也被覆盖,不符合定义,因此 \(f_{i,j}=f_{i-1,j}\)
- \(j\ge r_i\):选了这个区间,那么就是一种方案。不选也是一种方案。相当于再原来方案数上又多了一种选择,因此 \(f_{i,j}=2f_{i-1,j}\)
- \(j=r_i\):虽然这个已经在第二种情况讲过了,但是第二种情况的 \([l_i,r_i]\) 只是一个可有可无的区间,所以第三种情况介绍的是这段区间对连接的作用,我们只要想想有哪些状态与这个区间连接可以构成我们的 \(1\sim j\),很明显是 \(f_{i-1,k}(l_i-1\le k\le r_i-1)\),因此 \(f_{i,j}+\sum_{k=l_i-1}^{r_i-1} f_{i-1,k}\to f_{i,j}\)
上述转移过程需要按顺序转移。
- 初始化:\(f_{1,0}=1\)
注意到我们的第三种情况需要用到 \(f_{i-1,k}(l_i-1\le k\le r_i-1)\),如果我们的区间的左端点不是从小到大排序的话,那么本来要用到且存在的值就不见了,为了保证区间处理顺序从左到右,左侧覆盖由前面的区间完备处理,符合 DP 状态的顺序性。要按左端点从小到大排序。
如果左端点相同,右端点又该怎么办呢?假设现在有两段区间 \([1,3],[1,4]\),我们枚举到了一个 \(j=3\),且当前 \(f_j=0\),如果先遍历 \([1,3]\),再遍历 \([1,4]\) 的话,那么最终 \((f_j+p) \times 2\to f_j\),其中 \(p\) 表示贡献。但是我们的贡献 \(p\) 只能被算一次(因为这是跟别的区间拼起来的,不是可有可无的),因此我们要在左端点相同时,则按右端点从大到小排序。
综上,我们的排序方式就是按左端点从小到大排序,若左端点相同,则按右端点从大到小排序。
然后 DP 过程先把第一维滚掉,然后线段树优化就完了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 10;
const int mod = 1e9 + 7;
struct segment_tree {
int sumv[4 * N], lazy[4 * N];
void pushup(int id) {
sumv[id] = (sumv[id * 2] + sumv[id * 2 + 1]) % mod;
}
void pushdown(int id, int l, int r) {
int mid = (l + r) >> 1;
sumv[id * 2] = sumv[id * 2] * lazy[id] % mod;
lazy[id * 2] = lazy[id * 2] * lazy[id] % mod;
sumv[id * 2 + 1] = sumv[id * 2 + 1] * lazy[id] % mod;
lazy[id * 2 + 1] = lazy[id * 2 + 1] * lazy[id] % mod;
lazy[id] = 1;
}
void update_mul(int id, int l, int r, int x, int y, int v) {
if (x <= l && r <= y) {
sumv[id] = sumv[id] * v % mod;
lazy[id] = lazy[id] * v % mod;
return ;
}
int mid = (l + r) >> 1;
pushdown(id, l, r);
if(x <= mid) update_mul(id * 2, l, mid, x, y, v);
if(y > mid) update_mul(id * 2 + 1, mid + 1, r, x, y, v);
pushup(id);
}
void update_add(int id, int l, int r, int x, int v) {
if (l == r) {
sumv[id] = (sumv[id] + v) % mod;
return ;
}
int mid = (l + r) >> 1;
pushdown(id, l, r);
if (x <= mid) update_add(id * 2, l, mid, x, v);
if (x > mid) update_add(id * 2 + 1, mid + 1, r, x, v);
pushup(id);
}
int query_sum(int id, int l, int r, int x, int y) {
if (x <= l && r <= y) return sumv[id];
int mid = (l + r) >> 1, ans = 0;
pushdown(id, l, r);
if (x <= mid) ans = (ans + query_sum(id * 2, l, mid, x, y)) % mod;
if (y > mid) ans = (ans + query_sum(id * 2 + 1, mid + 1, r, x, y)) % mod;
return ans;
}
} seg;
struct node {
int l, r;
} p[N];
int n, m, pos[N];
bool cmp(node x, node y) {
if (x.l != y.l) return x.l < y.l;
return x.r > y.r;
}
vector<int> vec;
int get(int x) {
int l = 0, r = vec.size() - 1;
while (l <= r) {
int mid = (l + r) >> 1;
if (vec[mid] < x) l = mid + 1;
else if (vec[mid] > x) r = mid - 1;
else return mid + 1;
}
}
signed main() {
freopen("fall.in", "r", stdin);
freopen("fall.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> p[i].l >> p[i].r;
vec.push_back(p[i].l);
vec.push_back(p[i].r);
}
for (int i = 1; i <= m; i++) {
cin >> pos[i];
vec.push_back(pos[i]);
}
sort(pos + 1, pos + m + 1);
m = unique(pos + 1, pos + m + 1) - (pos + 1);
// sort(vec.begin(), vec.end());
// vec.erase(unique(vec.begin(), vec.end()), vec.end());
// for (int i = 1; i <= n; i++) {
// p[i].l = get(p[i].l);
// p[i].r = get(p[i].r);
// }
// for (int i = 1; i <= m; i++)
// pos[i] = get(pos[i]);
for (int i = 1; i <= n; i++) {
p[i].l = lower_bound(pos + 1, pos + m + 1, p[i].l) - pos;
p[i].r = upper_bound(pos + 1, pos + m + 1, p[i].r) - (pos + 1);
}
sort(p + 1, p + n + 1, cmp);
for (int i = 1; i <= 4 * m; i++) seg.lazy[i] = 1;
seg.update_add(1, 0, m, 0, 1);
for (int i = 1; i <= n; i++) {
seg.update_mul(1, 0, m, p[i].r, m, 2);
seg.update_add(1, 0, m, p[i].r, seg.query_sum(1, 0, m, p[i].l - 1, p[i].r - 1));
}
cout << seg.query_sum(1, 0, m, m, m) << endl;
return 0;
}
冬
- \(44\) 分做法(赛时做法)
打表可以发现,\(f_{44}>10^9\),因此将序列分成前面 \(1\sim 22\) 与后面 \(23\sim 44\),分别跑 DFS 枚举状态(选或不选),会得到两个集合,然后跑二分就完事了。
注:全场最高
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 85;
const int M = 1 << (22);
const int mod = 998244353;
int n, f[N];
map<int, int> mp;
vector<int> linker1, linker2;
void dfs1(int x, int now) {
if (now == 23) {
if (x <= n) linker1.push_back(x);
return ;
}
dfs1(x + f[now], now + 1);
dfs1(x, now + 1);
}
void dfs2(int x, int now) {
if (now == 44) {
if (x <= n) linker2.push_back(x);
return ;
}
dfs2(x + f[now], now + 1);
dfs2(x, now + 1);
}
signed main() {
freopen("winter.in", "r", stdin);
freopen("winter.out", "w", stdout);
cin >> n;
f[1] = 1, f[2] = 2;
for (int i = 3; i <= 43; i++)
f[i] = f[i - 1] + f[i - 2];
dfs1(0, 1), dfs2(0, 23);
int ans = 0;
for (auto v : linker2)
mp[v]++;
for (auto v : linker1) {
// cout << v << endl;
ans = (ans + mp[n - v]) % mod;
}
cout << ans << endl;
return 0;
}
- 满分做法
数位 DP 会,但是代码要写 \(930\) 行,所以这段做法被吃掉了。

浙公网安备 33010602011771号