2024 牛客多校 1
0. preface
https://ac.nowcoder.com/acm/contest/81596
过题数
- \(n \geq 40\) ,几乎可补题。除非是高科技题。
- \(20 \geq n < 40\) ,酌情可补题。可能对得上技能树。
- \(n < 20\) ,几乎不可补题。除非是一些低科技的神秘启发题。
本场共 \(11\) 题,可补题有 \(8\) 题。
- \(A\) 简单数学题
- \(B\) 基础数学题 + 动态规划
- \(C\) 简单注意力题/数学题
- \(D\) 正常题
- \(E\) 多个高科技嵌套题。不可补。
- \(F\) 模拟 + DP 题 + 可被单调栈优化的转移。有启发意义。
- \(G\) 高科技题。不可补。
- \(H\) 简单思维题
- \(I\) tarjan + 模拟。
- \(J\) 中级科技题嵌套 + 启发题 + 工业题。精神好的时候可补。
- \(K\) 神秘图论题。不可补。
1. H
https://ac.nowcoder.com/acm/contest/81596/H
题意
有两场 World Finals 同时举办,每场都有若干出线队伍,两场出线名单可能有重复的队伍。一个队伍只能选择其中的一场参加,然后每场比赛的每支队伍都有一个预测成绩(过题数 + 罚时)。
lzr010506 在两场 World Finals 都出线了,他想知道如果所有队伍的最终成绩就是预测成绩,并且可以任意分配两场都出线了队伍的比赛场次的选择,他们队最高可能的排名是多少。
保证不会出现同题同罚时。
题解
注意到比赛场数是常数。考虑 \(lzr010506\) 能选择两场比赛,不妨考虑两场比赛的结果。取排名的更低值。
考虑任意一场比赛,既然只要求高排名,不妨让所有获得两场名额的队伍都假设为参加另一场。
实现
想想就感觉 STL 搞来搞去很烦。题目钦定了顺序不妨用结构体顺便重载顺序。
字符串不需要离散化,不触发字符串的比较符可以保证复杂度。
首先就是一个离线。为了记每个队伍在哪场出现过,一个状压(常数)。
然后继续遍历筛选只在这场会出现的或 \(lzr010506\) 。
然后排序。暴力找一下位置就行了。
时间反正是 \(O(n \log n)\) 的。
手速狗的养成……
Code
struct Team {
std::string name;
int nums, times;
Team () {}
Team (std::string name_, int nums_, int times_) {
name = name_;
nums = nums_;
times = times_;
}
bool operator < (const Team& o) const {
if (nums != o.nums) {
return nums > o.nums;
} else {
return times < o.times;
}
}
};
void solve() {
std::map<std::string, int> occur;
int n; std::cin >> n;
std::vector<Team> vec1(n + 1);
for (int i = 1; i <= n; i++) {
std::string s; int nums; int times;
std::cin >> s >> nums >> times;
vec1[i] = {s, nums, times};
occur[s] |= 1 << 0;
}
int m; std::cin >> m;
std::vector<Team> vec2(m + 1);
for (int i = 1; i <= m; i++) {
std::string s; int nums; int times;
std::cin >> s >> nums >> times;
vec2[i] = {s, nums, times};
occur[s] |= 1 << 1;
}
const std::string t = "lzr010506";
std::vector<Team> vec3;
for (int i = 1; i <= n; i++) {
std::string s = vec1[i].name;
int nums = vec1[i].nums;
int times = vec1[i].times;
if (s == t || (occur[s] >> 0 & 1 && ~ (occur[s] >> 1) & 1)) {
vec3.push_back(vec1[i]);
}
}
std::sort(vec3.begin(), vec3.end());
const int INF = 1 << 30;
int ans = INF;
for (int i = 0; i < vec3.size(); i++) {
// std::cout << vec3[i].name << " " << vec3[i].nums << " " << vec3[i].times << "\n";
if (vec3[i].name == t) {
ans = std::min(i + 1, ans);
break;
}
}
vec3.clear();
for (int i = 1; i <= m; i++) {
std::string s = vec2[i].name;
int nums = vec2[i].nums;
int times = vec2[i].times;
if (s == t || (occur[s] >> 1 & 1 && ~ (occur[s] >> 0) & 1)) {
vec3.push_back(vec2[i]);
}
}
std::sort(vec3.begin(), vec3.end());
for (int i = 0; i < vec3.size(); i++) {
if (vec3[i].name == t) {
ans = std::min(i + 1, ans);
break;
}
}
std::cout << ans << "\n";
}
如果问题变为排名比率最高怎么办?不妨让当所有前一场的两场名额都有的比 \(lzr010506\) 更强的队伍都假设为参加另一场。强度值显然按过题数升序,罚时降序。
2. C
https://ac.nowcoder.com/acm/contest/81596/C
题意
维护一个初始为空的非负整数序列,支持 \(q\) 次操作 : \(x, v\)。
每次操作移除末尾的 \(x\) 个整数,然后在末尾加入一个整数 \(v\) 。(空序列移除末位数后仍是空序列)
每次操作后输出当前序列所有后缀和的总和。
答案对 \(1 000 000 007 (10^{9} + 7)\) 取模。
题解
考虑一个序列 \([a_1, a_2, \cdots, a_n]\) 的所有后缀和的总和为 \(cost = \sum_{i = 1}^{n} i \times a_i\) 。直接维护 \(cost\) 可以做到 \(O(1)\) 询问。
考虑维护一个前缀数组 \([a_1, a_2, \cdots, a_n]\) ,暴力弹出并修改 \(cost\) 。因数组只会增长 \(O(q)\) 次,所以 \(q\) 次暴力弹出的均摊复杂度是每次 \(O(1)\) 。
时间复杂度 \(O(q)\) 。
Code
const int MOD = 1000000007;
void solve() {
int n; std::cin >> n;
i64 cost = 0;
std::vector<i64> a;
for (int i = 1; i <= n; i++) {
int x, v; std::cin >> x >> v;
for (int j = 0; j < x; j++) {
if (a.size()) {
cost = (cost - a.back() * a.size() % MOD + MOD) % MOD;
a.pop_back();
}
}
a.push_back(v);
cost = (cost + a.back() * a.size() % MOD) % MOD;
std::cout << cost << "\n";
}
}
3. A
先说教育意义,数和数位分离考虑的好题。
题意
求有多少长为 \(n\) 的元素是 \([0, 2^m)\) 的整数序列,满足存在一个非空子序列的 \(AND\) 和是 \(1\) 。答案对输入的正整数 \(q\) 取模。
\(1 \leq n, m \leq 5000, 1 \leq q \leq 10^{9}\)
题解
首先想到 \(q\) 不保证是质数,于是没有逆元。猜想最终答案可以写成一个组合数形式,只需帕斯卡公式递推组合数。
第一个思路尝试
问题可以想成。对二进制下的低 \(m\) 位任取一个 \(0\) 或 \(1\) 后构成的数组成集合 \(\mathbb{S}\) 。从 \(\mathbb{S}\) 中可重复地任选 \(n\) 个数组成序列 \(p\) 。询问有多少种 \(p\) 满足 \(p\) 中存在一个子序列的 \(AND\) 和是 \(1\) 。
这样想的代价是什么?首先要把 \(\mathbb{S}\) 集合给确定出来,这就是一个 \(2^{m^{n}} = 2^{mn}\) 。爆炸了。
第二个思路尝试
换一种思路。问题可以想成。钦定一个序列的 \(n\) 个数,每个数有 \(m\) 个 bit 。每个 bit 任选 \(0/1\) 的情况下,有多少种方案数满足:存在一个子序列, \(AND\) 和是 \(1\) 。
既然这样,于是选 \(k\) 个数钦定最低位是 \(1\) ,剩下的 \(n - k\) 个数钦定最低位是 \(0\) 。
考虑一个经典的思路:按位考虑。
- 最低位是 \(0\) 的数,一定不会贡献给 \(AND\) 和是 \(1\) 的子序列。
- 于是这 \(n - k\) 个数的前 \(m - 1\) 个位置可以任意选,每一位上有 \(2^{n - k}\) 种选择。所有位的方案数按乘法原理是 \(2^{{n - k}^{m - 1}} = 2^{(n - k)(m - 1)}\) 。
- 最低位是 \(1\) 的数,只需存在一个子序列满足 \(AND\) 和是 \(1\) ,即存在一个子序列的前 \(m - 1\) 位的 \(AND\) 和是 \(0\) 。
- 正难则反,不难考虑到 “存在一个子序列的前 \(m - 1\) 位的 \(AND\) 和是 \(0\)” 等于 “样本空间” - “子序列的前 \(m - 1\) 为的 \(AND\) 和恒为 \(1\) ” 。
- 于是前 \(m - 1\) 个位置,每个位置有 \(2^{k} - 1\) 种选择。所有位的方案数按乘法原理是 \((2^{k} - 1)^{m - 1}\) 。
于是存在这种一个子序列的 \(AND\) 和是 \(1\) 的序列的数量是
面对 \(1 \leq n, m \leq 5000\) 的数据,组合数显然可以递推,也用不到逆元。于是问题可以解决。
有时候注意一下组合数的范围不一定是和 \(n\) 同阶,这里是同阶。
预处理组合数 \(O(n^{2})\) ,计算数量 \(O(n \log nm)\) 。
Code
int n, m, q; std::cin >> n >> m >> q;
std::vector<std::vector<i64> > C(n + 1, std::vector<i64>(n + 1));
C[0][0] = 1;
for (int i = 1; i <= n; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % q;
}
}
auto ksm = [&] (i64 a, i64 n) -> i64 {
i64 res = 1; a %= q;
for (;n;n>>=1,a=a*a%q)if(n&1)res=res*a%q;
return res;
};
i64 ans = 0;
for (i64 k = 1; k <= n; k++) {
i64 P = (C[n][k] * ksm(2, 1LL * (n - k) * (m - 1))) % q;
i64 Q = (ksm(ksm(2, k) - 1, m - 1) + q) % q;
ans = (ans + P * Q % q) % q;
}
std::cout << ans << "\n";
4. B
题意
求有多少长为 \(n\) 的元素是 \([0, 2^m)\) 的整数序列,满足存在两个不同的非空子序列的 \(AND\) 和是 \(1\) 。答案对输入的正整数 \(q\) 取模。
\(1 \leq n, m \leq 5000, 1 \leq q \leq 10^{9}\)
题解
敲黑板,仔细分析。这题典型处理挺多。
考点一:
首先动动脑子上点智力。存在两个不好考虑,但是可以正难则反地容斥。即 \(\geq 2\) ,他的答案为 \(\geq 1\) 减去 \(= 1\) 。
首先,存在多少长为 \(n\) 的元素是 \([0, 2^{m})\) 的整数序列,满足存在非空子序列的 \(AND\) 和是 \(1\) ?直觉上不会难算,实际上 \(A\) 题就是。
然后,存在多少长为 \(n\) 的元素是 \([0, 2^{m})\) 的整数序列,满足严格有一个非空子序列的 \(AND\) 和是 \(1\) ?
考点二:
考虑钦定一个序列的 \(n\) 个数,选 \(k\) 个的最低位钦定为 \(1\) ,另外 \(n - k\) 个数的最低位钦定为 \(0\) 。不难想到最低位是 \(0\) 的数前 \(m - 1\) 位可以任选,因为不会对“\(AND\) 和是 \(1\) 的非空子序列”产生贡献。最低位是 \(0\) 的这一部分数共有方案数 \(2^{(n - k)(m - 1)}\) (按位考虑,推导思路和 \(A\) 题一样)。先选出他们,方案数为
考点三:
考虑最低位是 \(1\) 的数,考虑这 \(k\) 个数严格只有一个非空子序列满足 \(AND\) 和是 \(1\) 。
- 首先如果这 \(k\) 个数存在一个非空真子序列满足 \(AND\) 和是 \(1\) ,则这个非空真子序列的超序列也满足 \(AND\) 和是 \(1\) 。于是这 \(k\) 个数就是这个子序列。
- 问题变成了,这 \(k\) 个数的前 \(m - 1\) 位,每位的与和都是 \(0\) ,且任意删掉一个数会导致与和变为 \(1\) 。
分析这个新问题。考虑某一位,一定存在两个很强的限制:
- 如果不存在 \(0\) 则一定导致这一位的与和为 \(1\) 。
- 如果存在 \(2\) 个 \(0\) ,则删去任意一个数,这位上的与和依旧为 \(0\) 。
考虑某位上存在且仅存在 \(1\) 个 \(0\) ,为了方便说明,将它称为一个特殊位。
考点四:
称一个特殊位对应一个数,当且仅当这个数被删除时,改特殊位上的 \(0\) 会被删除。于是:
- 当 \(k \geq 2\) ,前 \(m - 1\) 位上存在有 \(k\) 个特殊位和 \(k\) 个最低位为 \(1\) 的数一一对应。
- 当 \(k = 1\) ,不在乎特殊位,只需要前 \(m - 1\) 位上全是 \(0\) 。
考点五:
设 \(dp_{i, j}\) 为 \(i\) 个数中存在 \(j\) 个特殊位的方案数,且每个数至少对应一个特殊位。
联想到斯特林数的递推,每个特殊位相当于一个球,每个数相当于一个非空箱子。
考虑当前有 \(i\) 个数字,存在 \(j\) 个特殊位。若特殊位加一,会造成的影响。
- 这个特殊位上的 \(0\) 可以属于到这 \(i\) 个数的一个。考虑属于哪个数,共有 \(i\) 种可能。于是 \(dp_{i, j + 1} += i \times dp_{i, j}\) 。
- 这个特殊为上的 \(0\) 可以不属于这 \(i\) 个数的一个。考虑新增一个数的位置在哪,原来 \(i\) 个数贡献了 \(i + 1\) 个空位,于是有 \(i + 1\) 种可能。于是 \(dp_{i + 1, j + 1} += (i + 1) \times dp_{i, j}\) 。
进行一个归纳得到
考虑初始化, \(0\) 个箱子 \(0\) 个球的方案数为 \(dp_{0, 0} = 1\) 。然后 \(O(n m)\) 递推。
\(k\) 个最低位为 \(1\) 的数最终可能的方案数为
当钦定了 \(t\) 个特殊位后,剩下 \(m - 1 - t\) 个位置上需要与和为 \(1\) 且不只有 \(1\) 个 \(0\) 。即当 \(k \geq 2\) 时需要减去全 \(1\) 的方案数和只有一个 \(0\) 的方案数,当 \(k = 1\) 时减去全 \(1\) 的方案数。
于是存在且仅存在一个子序列的答案根据乘法原理为
归纳出存在两个子序列的答案为
计算答案的时间复杂度为 \(O(n m)\) 。组合数的时间复杂度为 \(O(max(n, m)^{2})\) ,注意组合数的值域范围为 \(max(n, m)\) 。\(dp\) 复杂度为 \(O(n m)\) 。
考点六:
最后,这 byd 题目卡非常量的取模速度了。
两种优化之一可以通过:
- 优化掉快速幂:少一个 \(\log n\) 。
- 使用动态模数:模动态模时速度 \(\times 2\) ,模静态为负优化。
优化掉快速幂
int n, m, q; std::cin >> n >> m >> q;
const int N = std::max(n, m);
std::vector<std::vector<i64> > C(N + 1, std::vector<i64>(N + 1));
C[0][0] = 1;
for (int i = 1; i <= N; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % q;
}
}
std::vector<std::vector<i64> > dp(n + 1, std::vector<i64>(m + 1));
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = (i * ((dp[i - 1][j - 1] + dp[i][j - 1]) % q)) % q;
}
}
std::vector<int> pw2(N + 1);
pw2[0] = 1;
for (int i = 1; i <= N; i++) {
pw2[i] = (pw2[i - 1] << 1) % q;
}
std::vector<i64> f(n + 1), g(n + 1);
i64 ans = 0;
for (i64 k = 1; k <= n; k++) {
if (k == 1) {
g[k] = 1;
}
f[k] = C[n][k];
i64 P = 1; // ^{0}
i64 H = 1; // ^{0}
for (int j = m - 1; j >= 1; --j) {
f[k] = (f[k] * pw2[n - k]) % q;
P = (P * (pw2[k] - 1 + q)) % q;
if (k > 1 && j >= k) {
i64 Q = (C[m - 1][j] * dp[k][j]) % q;
g[k] = (g[k] + Q * H % q) % q;
}
H = (H * (pw2[k] - 1 - k + q)) % q;
}
ans = (ans + f[k] * (P - g[k]) % q) % q;
}
ans = (ans + q) % q;
std::cout << ans << "\n";
不优化快速幂但是动态取模
struct DynamicModInt { // 动态模加速
i128 base = 1;
int mod;
void init(int mod_){
mod = mod_;
base = (base << 64) / mod;
}
i64 operator () (i64 x) { // 自动取非负数
i64 res = x - mod * (base * x >> 64);
return res == mod ? 0 : res;
}
} mol;
void solve() {
i64 n, m, q; std::cin >> n >> m >> q;
mol.init(q);
const int N = std::max(n, m);
std::vector<std::vector<i64> > C(N + 1, std::vector<i64>(N + 1));
C[0][0] = 1;
for (int i = 1; i <= N; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = mol(C[i - 1][j - 1] + C[i - 1][j]);
}
}
std::vector<int> pw2(N * N + 1);
pw2[0] = 1;
for (int i = 1; i <= N * N; i++) {
pw2[i] = mol(pw2[i - 1] << 1);
}
auto ksm = [&] (i64 a, i64 n) -> i64 {
i64 res = 1; a = mol(a + q);
for (;n;n>>=1,a=mol(a*a))if(n&1)res=mol(res*a);
return res;
};
i64 ans = 0;
for (i64 k = 1; k <= n; k++) {
i64 P = mol(C[n][k] * ksm(2, 1LL * (n - k) * (m - 1)));
i64 Q = mol(ksm(ksm(2, k) - 1, m - 1));
ans = mol(ans + P * Q);
}
std::vector<std::vector<i64> > dp(n + 1, std::vector<i64>(m + 1));
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = mol( i64(i) * mol(dp[i - 1][j - 1] + dp[i][j - 1]) );
}
}
std::vector<i64> f(n + 1), g(n + 1);
for (i64 k = 1; k <= n; k++) {
f[k] = mol(C[n][k] * pw2[(n - k) * (m - 1)]);
if (k == 1) {
g[k] = 1;
} else {
for (i64 t = k; t <= m - 1; t++) {
i64 Q = mol(C[m - 1][t] * dp[k][t]);
i64 H = ksm(pw2[k] - 1 - k, m - 1 - t);
g[k] = mol(g[k] + mol(Q * H));
}
}
ans = mol(ans - mol(f[k] * g[k]));
}
ans = mol(ans + q);
std::cout << ans << "\n";
}
5. I
题意
有一个 \(n \times m\) 的矩形镜子迷宫,镜子有 “, /, -, |” 四种,每种镜子有特定的光线反射方向,注意直接通过镜子的情况不算被反射。
有 \(q\) 个询问,每个询问给定一个点光源 \((x, y, dir)\) ,表示在 \((x, y)\) 位置向 \(dir\) 方向发射一束光线,问经过足够的时间之后,这束光线被多少个不同的镜子反射过。
\(1 \leq n, m \leq 1000, 1 \leq q \leq 10^5\)
题解