Codeforces Round 1122 (Div. 3)
点击查看题面
A. Good Contest
题目大意
在一场共有
记分榜数据丢失,仅保留了每道题目的通过人数统计数组
数据范围:
- 测试用例组数
满足 。 - 参赛选手总人数
满足 。 - 每道题解出人数
满足 (其中 )。
思路
本题的核心目标是在给定各题解出总人数的约束下,最小化未解出所有题目的选手人数。
- 目标转化: 设解出全部
道题目的选手数量为 。根据题意,weak 选手即为未完全解出所有题目的选手,其数量为:
因此,最小化 weak 选手数量
- 理论上界推导: 任何解出全部
道题目的选手,必然对每道题目的通过人数均贡献了 。因此,解出全部题目的选手总数 受限于通过人数最少的那道题,即:
- 构造性证明(上界可达性): 设
。我们可采取如下贪心分配方案:
- 选取编号为
的 位选手,令他们均解出全部三道题目。此时每道题目均被消耗了 个解出名额。 - 对于剩余的名额,第
道题还剩余 个名额。由于 ,必存在至少一个维度使得剩余名额为 。 - 将剩余的名额任意分配给编号在
到 的选手(每名选手在同一题上至多分配一次)。由于至少有一道题的剩余名额为 ,后续选手不可能同时获得全部三题的解出判定。
因此,该构造方案严格满足题目给定的统计值
- 最终结果: 最小化 weak 选手数量为:
复杂度分析
- 时间复杂度:对于每组测试用例,仅需对三个数取最小值并执行一次减法操作,单组用例时间复杂度为
。处理全部 组测试用例的总时间复杂度为 。 - 空间复杂度:算法仅需常数级别的变量存储输入与计算结果,空间复杂度为
。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void fc() {
int n, a, b, c;
std::cin >> n >> a >> b >> c;
std::cout << n - std::min({a, b, c}) << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}B. Three Piles
题目大意
Alice 和 Bob 正在用三堆石子玩博弈游戏。初始时,Alice 拥有
两人轮流行动,Alice 先手。在每个回合中,当前行动的玩家可以从第三堆石子中取走任意数量的石子(可以取
设游戏结束时 Alice 和 Bob 最终拥有的石子数分别为
数据范围:
测试用例组数
满足 。 每堆石子的初始数量均满足
。
思路
本题属于完全信息博弈中的极小化极大(Minimax)问题。Alice 追求差值绝对值
- 策略一:Alice 首轮取尽公共石子
- 由于 Alice 拥有先手优势,且规则允许一次性取走任意数量的石子,Alice 可以在第
回合直接取走第三堆全部的 颗石子。 - 此时第三堆石子被清空(剩余数量为
)。接下来的第 回合 Bob 只能取 颗;第 回合 Alice 也只能取 颗。连续两次取 导致游戏强制终止。 - 此时双方最终石子数分别为
和 ,最终得分为:
- 因为 Alice 可以单方面主动触发该分支,所以 Alice 保底可以获得不低于
的收益。
- 策略二:Alice 首轮取
颗石子
- 若 Alice 在第
回合选择取 颗石子,此时轮到 Bob 行动: - 若 Bob 同样选择取
颗石子:构成连续两轮取 ,游戏立即结算,得分为:
- Bob 是否有动力取走
颗石子? - 当
时,Bob 的石子数本身已不低于 Alice。若 Bob 取走 颗石子,其持有量变为 ,与 Alice 现存的 的差距被进一步拉大( )。随后又将轮到 Alice 行动,Alice 可顺势收割剩余石子扩大分差。追求极小化分差的 Bob 绝不会选择使局面差距恶化的 ,其最优应对只能是同样取 结束游戏,锁定得分 。 - 当
时,容易证明 ,即 。此时 Alice 直接采取策略一即可获得更优结果,无须考虑此分支。
- 博弈均衡解 Alice 在开局面临两种完全可控的路径选择:要么直接清空石子堆锁定
,要么通过取 迫使 Bob 接受当前初始差值 。追求收益最大化的 Alice 必然会在两者之间取最大值:
复杂度分析
- 时间复杂度:对于每组测试用例,仅需执行常数次加减、绝对值计算与最值比较,单组用例时间复杂度为
。处理全部 组测试用例的总时间复杂度为 。 - 空间复杂度:仅使用常数级别的标量变量存储输入与计算中间值,辅助空间复杂度为
。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void fc() {
int a, b, c;
std::cin >> a >> b >> c;
std::cout << std::max(std::abs(a + c - b), std::abs(a - b)) << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}C. AND, OR, Sort!
题目大意
给定一个长度为
- 选择一个下标
( ),并将 替换为前缀 的按位与(bitwise AND)或按位或(bitwise OR)。
目标是通过最少的操作次数,使字符串
数据范围:
测试用例组数
满足 。 字符串长度
满足 。 所有测试用例的
之和不超过 。
思路
本题的关键在于挖掘位运算操作的传递性与首位元素的不可变性。
首元素的不可变性 当选取
时,前缀仅包含元素 本身。根据定义,单个元素的按位与及按位或均等于其自身。因此, 的取值在任何操作序列下均无法被更改。 分类讨论:
的情形
- 由于
且不可被修改,若要使整个序列非递减有序,所有后续位置 的最终取值必须全部为 。 - 因为首位固定为
,对于任意位置 ,前缀 中必然存在 ,其前缀或恒为 。 - 任何原先为
的字符只需执行 次前缀或操作即可变为 ;原本为 的字符则无需改动。 - 因此,所需的最少操作次数即为原字符串中
的总数,即:
- 分类讨论:
的情形
- 当首位为
时,序列最终必然会被划分为两部分:前 个字符均为 ,后 个字符均为 (其中分界点 满足 )。 - 置
的可行性:因为 ,任何位置 的前缀中必然包含 ,其前缀与恒为 。故任意原本为 的位置均可通过 次前缀与操作重置为 。 - 置
的可行性:一个位置要能变为 ,要求该位置之前(或当前位置)存在至少一个 以提供前缀或的有效来源。 - 代价函数推导: 若选定分界点为
(前 个为 ,后 个为 ): - 前缀区间
中原有的 需全部修改为 ,花费为该区间内 的个数,即 。 - 后缀区间
中原有的 需全部修改为 ,花费为该区间内 的个数,即 。 - 目标代价为:
- 分界点合法性与单调性分析:
- 若原串不存在
(全 ),则不可能凭空产生 ,序列只能保持全 ,对应 时 ,结果恒最优。 - 若原串存在
,设首个 出现的下标为 。对于所有 的分界点,前缀区间全为 ( ),代入公式得 。随着 单调增加,该代价严格单调递减。因此在 的无效区域内,代价函数单调递减,其最小值必然会在合法的 区域内被更优解覆盖,不会导致非法状态被选取。 - 当
时,前缀已覆盖了至少一个现存的 。我们可先从左至右利用该 通过前缀或操作把后缀的 逐个置为 ;随后再利用 通过前缀与操作将前缀中冗余的 全部置为 。该构造保证了上述代价上界严格可达。
因此,维护前缀和数组并在线性时间内枚举最优分界点
复杂度分析
时间复杂度:构建前缀和数组需线性扫描原串,耗时
;遍历分界点 进行常数时间的转移与比较,耗时 。处理所有测试用例的总时间复杂度为 。 空间复杂度:需要一个大小为
的动态数组 pre记录前缀和信息,额外辅助空间复杂度为。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void fc() {
int n;
std::string s;
std::cin >> n >> s;
std::vector<int> pre(n + 1);
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i] + s[i] - '0';
}
if (s[0] == '1') {
std::cout << n - pre[n] << '\n';
return;
}
int ans = INT_MAX;
for (int i = 0; i <= n; i++) {
ans = std::min(ans, pre[i] + n - i - (pre[n] - pre[i]));
}
std::cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}D. Falling Concrete
题目大意
Vihaan 正在修补一条由
他拥有一辆叉车,可以执行以下操作任意次(可以为
选择两个下标
(满足 ),叉起第 个路段并将其向后移至位置 。 在搬运过程中,被搬运的路段每越过一个路段就会掉落
个单位的混凝土到该路段上。随后,被搬运的路段插入到位置 。 形式化地,子数组
会被替换为:
如果道路的某一部分是一个由若干高度相同的路段构成的连续区间,则称该部分是平坦的(flat)。求经过任意次操作后,道路中平坦部分的最大可能长度。
数据范围:
测试用例组数
满足 。 路段数量
满足 。 初始路段高度
满足 。 所有测试用例的
之和不超过 。 题目保证在给定约束下,路段高度在任何操作序列下始终保持为正。
思路
- 寻找操作不变量(Invariant) 采用
坐标体系(即下标范围为 )考察单次操作:
- 原本位于位置
的路段被移至位置 ,其高度变为 。计算其高度与当前下标的差值:
- 原本位于区间
的任意路段 ,其位置右移至 ,高度变为 。计算其高度与当前下标的差值:
这表明:对于任意一个路段,无论经过何种合法操作序列,其「当前高度与所在下标的差值」
置换自由度(可达性分析) 当选取
时,操作将相邻的第 个元素移至位置 ,原位置 的元素顺延至位置 。该操作完全等价于一次相邻对换(Adjacent Transposition)。 在代数学中,相邻对换是对称群 的标准生成元集。因此,仅通过有限次相邻对换,即可在保持每个元素特征值 不变的前提下,将所有路段任意重排为集合 的任意排列。 平坦区间的代数特征 设最终方案中存在一个起始下标为
、长度为 的平坦区间,区间内每个路段的高度均等于常数 。 则对于该区间内的任意相对偏移量 ( ),处于位置 的路段其固有差值必须满足:
该式揭示了平坦区间的充分必要条件:
- 构成该平坦区间的
个元素,其固有差值集合必须呈现为公差为 的连续整数集合 (其中 )。 - 由于下标
严格递增,对应的差值 必须严格递减,因此相同的差值在同一个平坦区间内至多只能被使用一次。
- 问题转化与算法设计 问题最终等价于:在多重集
中,寻找一个元素互不相同且数值严格连续的最大子集。
- 将每个位置的特征值计算为
; - 对数组
进行升序排序; - 遍历排序后的序列,若遇到重复值则跳过(去重),若当前值等于前一个有效值加
,则连续长度累加;否则重置计数器; - 遍历过程中维护连续段的最大长度即可。
复杂度分析
时间复杂度:计算固有差值需要
;对长度为 的数组排序耗时 ;一次线性扫描统计最长连续段耗时 。对于全部测试用例,总时间复杂度为 ,在 的限制下耗时远低于 2.0 秒的时限。 空间复杂度:使用大小为固定常数的全局静态数组存储元素,排序仅需递归栈开销
,额外辅助空间复杂度为 。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N=2e5+5;
int n;
int a[N];
void fc() {
std::cin>>n;
for(int i=0;i<n;i++)std::cin>>a[i],a[i]-=i;
std::sort(a,a+n);
int ans=0,c=1;
for(int i=1;i<n;i++){
if(a[i]==a[i-1])continue;
if(a[i]==a[i-1]+1)c++;
else {
ans=std::max(ans,c);
c=1;
}
}
std::cout<<std::max(ans,c)<<"\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin>>t;
while (t--) fc();
return 0;
}E. Prime Destruction
题目大意
给定一个由
- 从多重集中选择一个大于
的整数 以及 的一个质因数 。将选取的该 从多重集中移除,并向多重集中加入 个数值为 的整数。
同时给定一个正整数
数据范围:
测试用例组数
满足 。 多重集初始大小
与目标上限 满足 。 多重集中的元素
满足 。 所有测试用例的
之和满足 。
思路
- 子问题独立性分析 每一次操作仅针对多重集中的单个数
进行质因数拆分,所生成的 个子元素 之间以及与其他原有元素之间互不影响。因此,各个元素的拆分过程完全相互独立。 若定义 为将单个整数 彻底拆分直至所有派生出的元素均满足 所需的最少操作次数,则根据独立性原理,全局最少操作次数可转化为各元素独立贡献之和:
- 动态规划状态设计与转移方程
- 边界状态:若
,该数无需执行任何分解操作即已满足条件,因此:
- 状态转移:对于
,必须对 至少执行一次操作。 - 若枚举选取
的某一质因数 (满足 且 ),当前操作消耗 次。 - 产生的
个子元素均为 ,后续为了使这 个子元素均分解至不超过 ,每个分支均需消耗 次操作,共计 次。 - 因此,选择质因数
的总代价为 。 - 为求得全局最优解,只需在
的所有不同质因数中取最小值:
- 拓扑序与质因数快速分解
- 由于质数
,严格保证了 。状态之间的转移关系构成一个天然的有向无环图(DAG),故 按照从小到大的顺序从 遍历至 进行递推即可。 - 为了快速获取任意整数
的所有互异质因数,可预先利用欧拉筛(线性筛)预处理出每个数的最小质因数 。对于每个状态 ,借助 可在对数时间内完成质因数分解与转移。
复杂度分析
时间复杂度:
预处理阶段:欧拉筛在
时间内求出 范围内的最小质因数。 单组测试阶段:DP 过程仅遍历区间
,由于不超过 的整数其互异质因数个数 (因为前 个质数之积 ),单点转移耗时为极小的常数级;统计答案仅需一次 的求和。 处理所有测试用例的总时间复杂度为
,远低于 2.0 秒的时限。 空间复杂度:
线性筛数组
minp、素数存储primes以及 DP 数组cnt均采用全局静态空间,空间复杂度为。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N=2e5+5;
int k,n;
int a[N],minp[N];
ll cnt[N];
std::vector<int>primes;
// 线性筛预处理最小质因数 minp
void init() {
for (int i = 2; i < N; ++i) {
if (!minp[i]) {
minp[i] = i;
primes.push_back(i);
}
for (int p : primes) {
if (i * p >= N) break;
minp[i * p] = p;
if (i % p == 0) break;
}
}
}
void fc() {
std::cin>>n>>k;
for(int i=1;i<=n;i++)std::cin>>a[i],cnt[i]=LLONG_MAX;
for(int i=0;i<=k;i++)cnt[i]=0;
for(int i=k+1;i<=n;i++){
ll base=2e9;
ll temp=i;
while(temp>1){
ll p=minp[temp];
base=std::min(base,1+p*cnt[i/p]);
while(temp%p==0)temp/=p;
}
cnt[i]=base;
}
ll ans=0;
for(int i=1;i<=n;i++)ans+=cnt[a[i]];
std::cout<<ans<<"\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
init();
std::cin>>t;
while (t--) fc();
return 0;
}