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\)

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

  1. 最低位是 \(0\) 的数,一定不会贡献给 \(AND\) 和是 \(1\) 的子序列。
    • 于是这 \(n - k\) 个数的前 \(m - 1\) 个位置可以任意选,每一位上有 \(2^{n - k}\) 种选择。所有位的方案数按乘法原理是 \(2^{{n - k}^{m - 1}} = 2^{(n - k)(m - 1)}\)
  2. 最低位是 \(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\) 的序列的数量是

\[\sum_{k = 1}^{n} \binom{n}{k} 2^{(n - k)(m - 1)} (2^{k} - 1)^{m - 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\) 题一样)。先选出他们,方案数为

\[f_{k} = \binom{n}{k} 2^{(n - k)(m - 1)} \quad 1 \leq k \leq n \]

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

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

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

  1. 如果不存在 \(0\) 则一定导致这一位的与和为 \(1\)
  2. 如果存在 \(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}\)

进行一个归纳得到

\[dp_{i, j} = i \times dp_{i, j - 1} + i \times dp_{i - 1, j - 1} = i \times (dp_{i, j} + dp_{i, j - 1}) \]

考虑初始化, \(0\) 个箱子 \(0\) 个球的方案数为 \(dp_{0, 0} = 1\) 。然后 \(O(n m)\) 递推。

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

\[g_k = \left \{ \begin{aligned} &1, &k = 1 \\ &\sum_{t = k}^{m - 1} \binom{m - 1}{t} dp_{k, t} (2^{k} - 1 - k)^{m - 1 - t}, &k \geq 2, \ k \leq t \leq m - 1 \\ \end{aligned} \right. \]

当钦定了 \(t\) 个特殊位后,剩下 \(m - 1 - t\) 个位置上需要与和为 \(1\) 且不只有 \(1\)\(0\) 。即当 \(k \geq 2\) 时需要减去全 \(1\) 的方案数和只有一个 \(0\) 的方案数,当 \(k = 1\) 时减去全 \(1\) 的方案数。

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

\[\sum_{k = 1}^{n} f_k \times g_k \]

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

\[\sum_{k = 1}^{n} f_k \times ((2^{k} - 1)^{m - 1} \times g_k) \]

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

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

两种优化之一可以通过:

  1. 优化掉快速幂:少一个 \(\log n\)
  2. 使用动态模数:模动态模时速度 \(\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\)

题解

6. D

7. G

8. F

posted @ 2024-08-24 10:26  zsxuan  阅读(7)  评论(0编辑  收藏  举报