2024 牛客多校 1

0. preface

https://ac.nowcoder.com/acm/contest/81596

过题数

  • n40 ,几乎可补题。除非是高科技题。
  • 20n<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(nlogn) 的。

手速狗的养成……

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 。(空序列移除末位数后仍是空序列)

每次操作后输出当前序列所有后缀和的总和

答案对 1000000007(109+7) 取模。

题解

考虑一个序列 [a1,a2,,an] 的所有后缀和的总和为 cost=i=1ni×ai 。直接维护 cost 可以做到 O(1) 询问。

考虑维护一个前缀数组 [a1,a2,,an] ,暴力弹出并修改 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,2m) 的整数序列,满足存在一个非空子序列的 AND 和是 1 。答案对输入的正整数 q 取模。

1n,m5000,1q109

题解

首先想到 q 不保证是质数,于是没有逆元。猜想最终答案可以写成一个组合数形式,只需帕斯卡公式递推组合数。

第一个思路尝试

问题可以想成。对二进制下的低 m 位任取一个 01 后构成的数组成集合 S 。从 S 中可重复地任选 n 个数组成序列 p 。询问有多少种 p 满足 p 中存在一个子序列的 AND 和是 1

这样想的代价是什么?首先要把 S 集合给确定出来,这就是一个 2mn=2mn 。爆炸了。

第二个思路尝试

换一种思路。问题可以想成。钦定一个序列的 n 个数,每个数有 m 个 bit 。每个 bit 任选 0/1 的情况下,有多少种方案数满足:存在一个子序列, AND 和是 1

既然这样,于是选 k 个数钦定最低位是 1 ,剩下的 nk 个数钦定最低位是 0

考虑一个经典的思路:按位考虑。

  1. 最低位是 0 的数,一定不会贡献给 AND 和是 1 的子序列。
    • 于是这 nk 个数的前 m1 个位置可以任意选,每一位上有 2nk 种选择。所有位的方案数按乘法原理是 2nkm1=2(nk)(m1)
  2. 最低位是 1 的数,只需存在一个子序列满足 AND 和是 1 ,即存在一个子序列的前 m1 位的 AND 和是 0
    • 正难则反,不难考虑到 “存在一个子序列的前 m1 位的 AND 和是 0 等于 “样本空间” - “子序列的前 m1 为的 AND 和恒为 1
    • 于是前 m1 个位置,每个位置有 2k1 种选择。所有位的方案数按乘法原理是 (2k1)m1

于是存在这种一个子序列的 AND 和是 1 的序列的数量是

k=1n(nk)2(nk)(m1)(2k1)m1

面对 1n,m5000 的数据,组合数显然可以递推,也用不到逆元。于是问题可以解决。

有时候注意一下组合数的范围不一定是和 n 同阶,这里是同阶。

预处理组合数 O(n2) ,计算数量 O(nlognm)

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,2m) 的整数序列,满足存在两个不同的非空子序列的 AND 和是 1 。答案对输入的正整数 q 取模。

1n,m5000,1q109

题解

敲黑板,仔细分析。这题典型处理挺多。

考点一:
首先动动脑子上点智力。存在两个不好考虑,但是可以正难则反地容斥。即 2 ,他的答案为 1 减去 =1

首先,存在多少长为 n 的元素是 [0,2m) 的整数序列,满足存在非空子序列的 AND 和是 1 ?直觉上不会难算,实际上 A 题就是。
然后,存在多少长为 n 的元素是 [0,2m) 的整数序列,满足严格有一个非空子序列的 AND 和是 1

考点二:
考虑钦定一个序列的 n 个数,选 k 个的最低位钦定为 1 ,另外 nk 个数的最低位钦定为 0 。不难想到最低位是 0 的数前 m1 位可以任选,因为不会对“AND 和是 1 的非空子序列”产生贡献。最低位是 0 的这一部分数共有方案数 2(nk)(m1) (按位考虑,推导思路和 A 题一样)。先选出他们,方案数为

fk=(nk)2(nk)(m1)1kn

考点三:
考虑最低位是 1 的数,考虑这 k 个数严格只有一个非空子序列满足 AND 和是 1

  1. 首先如果这 k 个数存在一个非空真子序列满足 AND 和是 1 ,则这个非空真子序列的超序列也满足 AND 和是 1 。于是这 k 个数就是这个子序列。
  2. 问题变成了,这 k 个数的前 m1 位,每位的与和都是 0 ,且任意删掉一个数会导致与和变为 1

分析这个新问题。考虑某一位,一定存在两个很强的限制:

  1. 如果不存在 0 则一定导致这一位的与和为 1
  2. 如果存在 20 ,则删去任意一个数,这位上的与和依旧为 0

考虑某位上存在且仅存在 10 ,为了方便说明,将它称为一个特殊位。

考点四:
称一个特殊位对应一个数,当且仅当这个数被删除时,改特殊位上的 0 会被删除。于是:

  • k2 ,前 m1 位上存在有 k 个特殊位和 k 个最低位为 1 的数一一对应。
  • k=1 ,不在乎特殊位,只需要前 m1 位上全是 0

考点五:
dpi,ji 个数中存在 j 个特殊位的方案数,且每个数至少对应一个特殊位。

联想到斯特林数的递推,每个特殊位相当于一个球,每个数相当于一个非空箱子。

考虑当前有 i 个数字,存在 j 个特殊位。若特殊位加一,会造成的影响。

  • 这个特殊位上的 0 可以属于到这 i 个数的一个。考虑属于哪个数,共有 i 种可能。于是 dpi,j+1+=i×dpi,j
  • 这个特殊为上的 0 可以不属于这 i 个数的一个。考虑新增一个数的位置在哪,原来 i 个数贡献了 i+1 个空位,于是有 i+1 种可能。于是 dpi+1,j+1+=(i+1)×dpi,j

进行一个归纳得到

dpi,j=i×dpi,j1+i×dpi1,j1=i×(dpi,j+dpi,j1)

考虑初始化, 0 个箱子 0 个球的方案数为 dp0,0=1 。然后 O(nm) 递推。

k 个最低位为 1 的数最终可能的方案数为

gk={1,k=1t=km1(m1t)dpk,t(2k1k)m1t,k2, ktm1

当钦定了 t 个特殊位后,剩下 m1t 个位置上需要与和为 1 且不只有 10 。即当 k2 时需要减去全 1 的方案数和只有一个 0 的方案数,当 k=1 时减去全 1 的方案数。

于是存在且仅存在一个子序列的答案根据乘法原理为

k=1nfk×gk

归纳出存在两个子序列的答案为

k=1nfk×((2k1)m1×gk)

计算答案的时间复杂度为 O(nm) 。组合数的时间复杂度为 O(max(n,m)2) ,注意组合数的值域范围为 max(n,m)dp 复杂度为 O(nm)

考点六:
最后,这 byd 题目卡非常量的取模速度了。

两种优化之一可以通过:

  1. 优化掉快速幂:少一个 logn
  2. 使用动态模数:模动态模时速度 ×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×m 的矩形镜子迷宫,镜子有 “, /, -, |” 四种,每种镜子有特定的光线反射方向,注意直接通过镜子的情况不算被反射。

q 个询问,每个询问给定一个点光源 (x,y,dir) ,表示在 (x,y) 位置向 dir 方向发射一束光线,问经过足够的时间之后,这束光线被多少个不同的镜子反射过。

1n,m1000,1q105

题解

6. D

7. G

8. F

posted @   zsxuan  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示