26牛客暑假多校3
点击查看题面
K题 Turn-by-Turn Navigation
题目大意
给定 LEFT)、右转(RIGHT)或直行(STRAIGHT)。保证路线中不包含
数据范围:
,
思路
相关算法:二维叉积
对于连续的三点
构造转向向量:
- 到达向量:
- 离开向量:
- 到达向量:
计算二维叉积:
注:叉积结果最大可达
,必须使用 64 位有符号整数( long long)计算。判定法则:
: 在 的逆时针方向,判定为 LEFT。 : 在 的顺时针方向,判定为 RIGHT。 :两向量共线。因无掉头,判定为 STRAIGHT。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void fc() {
int n;
cin >> n;
ll x0, y0, x1, y1, x2, y2;
cin >> x0 >> y0 >> x1 >> y1;
for (int i = 2; i < n; ++i) {
cin >> x2 >> y2;
ll ux = x1 - x0, uy = y1 - y0;
ll vx = x2 - x1, vy = y2 - y1;
ll k = ux * vy - uy * vx;
if (k > 0) cout << "LEFT";
else if (k < 0) cout << "RIGHT";
else cout << "STRAIGHT";
cout << (i == n - 1 ? "" : " ");
x0 = x1; y0 = y1;
x1 = x2; y1 = y2;
}
cout << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
if (cin >> t) {
while (t--) fc();
}
return 0;
}复杂度分析
- 时间复杂度:
。在线读取并处理每个点,单次计算为 ,总复杂度为 。 - 空间复杂度:
。消除了原代码中存储所有节点的 vector,实现常数级内存开销。
L题 Uphill Duel
题目大意
给定一个 First)还是后手胜(Second)。
数据范围:
思路
1. 博弈模型抽象
- 模型映射:本题属于单棋子在有限 DAG 上的常态博弈(Normal Play)。
- 状态退化:游戏不存在多个独立子游戏的组合,无需计算 Sprague-Grundy (SG) 函数,仅需判定每个状态的二值胜负性(必胜态 / 必败态)。
2. 状态转移法则
定义
- 必败态(Loss / P-state):节点
无任何相邻更高格子;或其所有相邻的更高格子均为必胜态。 - 必胜态(Win / N-state):节点
存在至少一个相邻更高格子 ,满足 为必败态。
转移方程:
3. 隐式拓扑序 DP
由于棋子只能向严格更高处移动,边方向为低高度指向高高度,图无环。
- 拓扑序:高度严格递减序列即为 DAG 的逆向拓扑序。
- 实现技巧:直接对所有节点按高度
降序排序,无需显式建立邻接表。按排序后的顺序遍历更新,可确保计算 时,所有更高的相邻节点 的状态 已确定。 - 转移剪枝:在检查四个方向时,一旦发现任意
为真,立刻置 并 break,无需继续扫描其他邻居。
参考代码
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
const int dx[4] = {1, -1, 0, 0};
const int dy[4] = {0, 0, 1, -1};
int a[N];
bool win[N];
int b[N];
int n, m;
void fc() {
cin >> n >> m;
int sz = n * m;
for (int i = 0; i < sz; i++) {
cin >> a[i];
b[i] = i;
win[i] = false;
}
sort(b, b + sz, [&](int x, int y) {
return a[x] > a[y];
});
for (int idx = 0; idx < sz; idx++) {
int u = b[idx];
int x = u / m, y = u % m;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
int v = nx * m + ny;
if (a[u] >= a[v]) continue;
if (!win[v]) {
win[u] = true;
break;
}
}
}
int k;
cin >> k;
while (k--) {
int x, y;
cin >> x >> y;
--x, --y;
cout << (win[x * m + y] ? "First\n" : "Second\n");
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
if (cin >> t) {
while (t--) fc();
}
return 0;
}复杂度分析
时间复杂度:
- 节点排序:
- 状态转移:每个节点最多检查
个邻居,为 - 单次查询:
,总查询 - 总时间复杂度:
,可通过。
- 节点排序:
空间复杂度:
- 静态数组存储高度、状态与拓扑索引,空间开销为
。
- 静态数组存储高度、状态与拓扑索引,空间开销为
A题 Bitmask
题目大意
给定一个长度为
数据范围:
思路
直接对每个数维护其二进制表示复杂度过高。注意到以下两个核心性质:
1. 连续 段的判别条件
在二进制表示中,一个极长连续
- 考虑到数值范围
,数值的有效位数为 位(共 30 位)。 - 定义第
位恒为 。 - 那么,对于任意数
, 等价于在二进制下满足“第 位为 且第 位为 ”的位对 的数量:
2. 按位操作的独立性
所有按位操作(&, |, ^)对二进制的每一位都是独立且并行的。
- 全局对所有
作用参数 时,第 位的新值仅取决于原数组在第 位的值以及 的第 位。 - 因此,我们可以拆分维护原数组在 相邻两位
上的频次分布。
实现
状态定义: 维护三维数组
cnt[j][v1][v2],表示当前数组中有多少个数满足:- 第
位的值为 - 第
位的值为 其中 。
- 第
初始化: 遍历输入数组
,将所有 的每对相邻位 的值统计入 cnt数组。转移维护: 对于每次操作
: - 提取
在第 位的值 ,在第 位的值 。 - 根据操作类型定义映射函数
: (AND): (OR): (XOR):
- 枚举旧状态
,更新至新状态 。
- 提取
答案统计: 每次修改后,所有
的 之和即为所有满足“低位为 、高位为 ”的计数之和:
复杂度分析
- 时间复杂度:
- 初始化预处理:
- 单次修改与统计:
- 总时间复杂度:
,对于 ,运算次数约为 ,可轻松通过。
- 初始化预处理:
- 空间复杂度:
- 辅助状态空间
为常数级别,总空间复杂度 。
- 辅助状态空间
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
int cnt[31][2][2];
int n, m;
int a[N];
inline int get(int x, int p) {
return (x >> p) & 1;
}
void fc() {
std::cin >> n;
for (int i = 0; i < n; i++) {
std::cin >> a[i];
for (int j = 0; j < 30; j++) {
cnt[j][get(a[i], j)][get(a[i], j + 1)]++;
}
}
std::cin >> m;
while (m--) {
int o, x;
std::cin >> o >> x;
auto f = [&](int val, int bit) -> int {
if (o == 1) return val & bit;
if (o == 2) return val | bit;
return val ^ bit;
};
for (int i = 0; i < 30; i++) {
int nxt[2][2] = {0};
int q = get(x, i), p = get(x, i + 1);
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
nxt[f(j, q)][f(k, p)] += cnt[i][j][k];
}
}
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
cnt[i][j][k] = nxt[j][k];
}
}
}
int ans = 0;
for (int i = 0; i < 30; i++) {
ans += cnt[i][1][0];
}
std::cout << ans << "\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
while (t--) fc();
return 0;
}B题 Buy One More
题目大意
小明初始有
求小明恰好喝完
数据范围:
(测试数据组数): : :
思路
算法原理
本题本质上是一个有限状态下的随机游走(Random Walk)与停时问题,核心在于通过资金平衡确定中奖次数,并利用 Raney 引理(Raney's Lemma / 循环引理) 求解满足前缀和约束的合法路径数。
1. 资金平衡方程
假设在喝完
- 初始资金为
元; - 喝完
瓶饮料共支出 元; - 中奖
次共获得 元。
若恰好在喝完第
因此,中奖次数
- 若
、 、 或 ,说明不可能恰好在第 瓶时花光钱,概率直接为 。 - 否则,问题转化为:在
次独立重复试验中恰好中奖 次,且在前 至 次喝饮料的过程中,小明手中的钱始终严格大于 (即不会提前破产),求该过程发生的概率。
2. 合法路径计数(Raney 引理)
定义第
- 若中奖,
; - 若未中奖,
。
前
根据 Raney 引理:对于任意一个总和为
因此,在所有
故合法排列方案数为:
结合每次中奖概率
应用场景
- 随机游走与格点路径计数:Raney 引理是处理带前缀和限制的路径计数(如 Catalan 数的推广、不相交路径)的有力工具。
- 概率破产模型:计算金融/博弈模型中资本在特定步数首次归零的概率分布。
补题中遇到的问题
- 问: 为什么是乘以
?
答: 乘以的本质逻辑在于:利用 Raney 引理(Raney's Lemma / Cycle Lemma) 将非对称的“中途资金时刻大于 ”前缀限制,转化为全局对称的“循环移位划分”。
i. 等价类分割: 将由次中奖(净变化 )与 次未中奖(净变化 )构成的 步序列放在环上,任意序列均可生成大小为 的循环等价类。
ii. 局部比例恒定: 整个序列净下降个单位。根据 Raney 引理,在此等价类的 个循环移位中,折线在下降过程中恰好产生 个突破历史新低的位置。以这 个位置的下一个位置作为起点,均能构造出一个在前 步资金保持 、第 步刚好降至 的合法序列。因此,每个等价类中的合法序列占比恒为 。
iii. 全局线性累加: 合法排列方案数为:
复杂度分析
时间复杂度:
- 预处理:预处理
范围内的阶乘及阶乘逆元,复杂度为 ,其中 。 - 单次查询:对于每组测试数据,仅需进行常数次模逆元与组合数计算,单次复杂度为
。 - 总时间复杂度:
。
空间复杂度:
- 空间复杂度:
,用于存储阶乘与逆元数组。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e6 + 5;
const ll MOD = 998244353;
ll fact[N], inv[N];
// 快速幂计算 (a^b) % MOD
ll qpow(ll a, ll b) {
ll res = 1;
for (a %= MOD; b > 0; b >>= 1, a = a * a % MOD) {
if (b & 1) res = res * a % MOD;
}
return res;
}
// 预处理阶乘与逆元,用于快速计算组合数
void init_fac(int n) {
fact[0] = 1;
for (int i = 1; i <= n; ++i) fact[i] = fact[i - 1] * i % MOD;
inv[n] = qpow(fact[n], MOD - 2);
for (int i = n - 1; i >= 0; --i) inv[i] = inv[i + 1] * (i + 1) % MOD;
}
// 计算组合数 C(n, k) % MOD
inline ll C(int n, int k) {
if (n < 0 || k < 0 || k > n) return 0;
return fact[n] * inv[k] % MOD * inv[n - k] % MOD;
}
ll n, m, c, a, b;
ll q, p; // q 中奖概率, p 不中奖概率
void fc() {
std::cin >> n >> m >> c >> a >> b;
// 计算满足最终资金归零所必需的中奖次数 k
ll k = (m - n) / c;
// 边界条件判断:无法恰好在第 m 瓶花光钱
if (n > m || (m - n) % c || k > m || k < 0) {
std::cout << "0\n";
return;
}
// 计算概率 q 与 p 的模逆元
q = a * qpow(b, MOD - 2) % MOD;
p = (b - a) * qpow(b, MOD - 2) % MOD;
// 根据 Raney 引理推导出的最终概率公式计算答案
ll ans = n * qpow(m, MOD - 2) % MOD * C(m, k) % MOD * qpow(q, k) % MOD * qpow(p, m - k) % MOD;
std::cout << ans << '\n';
}
int main() {
// 优化 I/O 读取效率
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
init_fac(N - 1);
std::cin >> t;
while (t--) fc();
return 0;
}