2026 CCPC 河北省赛
点击查看题面
A. 求求你不要再摔馍片了
题目大意
有
现在按初始顺序依次处理每包馍片,对每包馍片可选择是否将其扔到序列末尾(每包馍片至多被扔一次)。处理完毕后,按最终得到的顺序依次吃掉馍片。求能够获得的最大开心度。
数据范围:
思路
1. 问题本质拆解
每个元素至多被扔到末尾一次,且同一操作类别内的元素相对顺序保持不变。因此,原数组实际上被划分成了两个保持相对顺序的子序列:
- 保留在原地的子序列
; - 被移动至末尾的子序列
。
最终的食用顺序即为序列
基础开心度为每包
2. 状态设计
额外开心度的产生仅与前后相邻的味道相关,分为三部分:
- 序列
内部相邻元素的贡献; - 序列
内部相邻元素的贡献; - 序列
的末尾元素与序列 的首个元素连接时的跨段贡献。
由于味道数值极小(
:序列 当前末尾元素的味道( 表示 目前为空); :序列 的首个元素的味道( 表示 目前为空); :序列 当前末尾元素的味道( 表示 目前为空)。
定义
3. 状态转移
遍历输入序列中的每个
- 放入序列
: 新元素 接在 之后。若 ,增量为 ;若 ,增量为 。 状态转移至 。 - 放入序列
: - 若
(序列 为空),则 作为序列 的起始元素与当前末尾元素,此时 ; - 若
(序列 非空),新元素 接在 之后,增量为 。 状态转移至 。
4. 边界与答案合并
- 初始状态:
。 - 统计最终答案时,若序列
和序列 均非空(即 且 ),还需加上两段拼接时的贡献 。 - 遍历所有可能的状态取最大值,最终答案加上固定的基础开心度
。
复杂度分析
时间复杂度: 状态空间大小仅为
空间复杂度: 采用两组
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define int long long
void fc() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int& i : a) std::cin >> i;
int dp[6][6][6];
memset(dp, -1, sizeof(dp));
dp[0][0][0]=0;
for (int i = 0; i < n; i++) {
int ndp[6][6][6];
memset(ndp, -1, sizeof(ndp));
for (int x = 0; x <= 5; x++) {
for (int y = 0; y <= 5; y++) {
for (int z = 0; z <= 5; z++) {
if (dp[x][y][z] == -1) continue;
if(y==0){
ndp[x][a[i]][a[i]]=std::max(ndp[x][a[i]][a[i]],dp[x][0][0]);
}
int u = (x == 0 ? 0 : a[i] - x + 5) % 5 + dp[x][y][z];
int v = (z == 0 ? 0 : a[i] - z + 5) % 5 + dp[x][y][z];
ndp[a[i]][y][z] = std::max(ndp[a[i]][y][z], u);
if(y)ndp[x][y][a[i]] = std::max(ndp[x][y][a[i]], v);
}
}
}
memcpy(dp, ndp, sizeof(dp));
}
int ans = 0;
for (int i = 0; i <= 5; i++) {
for (int j = 0; j <= 5; j++) {
for (int k = 0; k <= 5; k++) {
if (dp[i][j][k] == -1) continue;
// std::cout << i << " " << j << " " << k << " " << dp[i][j][k] + (j - i + 5) % 5 << "\n";
ans = std::max(ans, dp[i][j][k] + (i == 0 || j == 0 ? 0 : j - i + 5) % 5);
}
}
}
std::cout << ans + n << "\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
// std::cin>>t;
while (t--) fc();
return 0;
}E. 电梯
题目大意
大楼共有
要求对于大楼中任意两层
求出电梯数量
数据范围:
- 所有电梯停靠的总层数满足
思路
1. 图论模型转化
本题可以抽象为完全有向无环图的路径覆盖问题:
- 将每一层看作图中的顶点
。 - 若电梯在
层与 层( )连续停靠,则等价于在图中包含一条有向边 。 - 题目要求任意两层均可直达,即图中的每条有向边
(共 条)都必须作为某个电梯停靠序列中的相邻点对出现至少一次。 - 每个电梯从第
层出发,单调递增停靠,最终到达第 层,对应一条从源点 到汇点 的有向路径。
2. 电梯数量 的理论下界
考虑对顶点集进行割划分。选取割
- 从集合
指向集合 的直达边总数为 。 - 由于电梯停靠楼层严格单调递增,任意一条从
到 的路径一旦从 进入 ,便无法再次回到 。 - 因此,任意一条路径至多只能覆盖一条跨越割集
的边。
由此可得电梯数量
3. 贪心路径拼接与构造
要达到理论下界,需要让尽可能多的边被复用进同一条路径中。利用队列数组 a[i] 维护当前末尾停靠在第
- 初始路径生成: 对于所有
,构建基础二元路径 放入 a[i],覆盖所有从第层出发的边 。 - 中间节点延伸(贪心匹配): 按顺序遍历中间楼层
。对于所有待覆盖的出边 ( ):
- 若
a[i]非空,说明已有以结尾的路径,直接取出一条路径在其末尾追加 ,并将其转移到 a[j]; - 若
a[i]为空,说明当前到达的路径已被耗尽,必须新开辟一条路径 压入 a[j]。
- 汇点收拢: 处理完所有出边后,遍历
,将所有未到达顶层的路径追加终点 ,合并至 a[n]。
该策略在前一半节点充分分裂路径以应对瓶颈宽度,在后一半节点高度复用并合并路径,最终生成的总路径数恰好达到下界
4. 资源限制验证
每条生成的路径长度至多为
复杂度分析
时间复杂度: 外层双重循环枚举每对 std::move 实现路径的所有权转移,单次转移为
空间复杂度: 队列容器中维护的所有向量元素总数即为电梯停靠的总层数,空间规模为
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
void fc() {
std::cin>>n;
std::vector<std::queue<std::vector<int>>>a(n+1);
for(int i=2;i<=n;i++){
std::vector<int>x={1,i};
a[i].push(std::move(x));
}
for(int i=2;i<n;i++){
for(int j=i+1;j<=n;j++){
if(a[i].empty()){
std::vector<int>x={1,i,j};
a[j].push(std::move(x));
}else{
a[i].front().push_back(j);
a[j].push(std::move(a[i].front()));
a[i].pop();
}
}
}
for(int i=1;i<n;i++){
while(a[i].size()){
a[i].front().push_back(n);
a[n].push(std::move(a[i].front()));
a[i].pop();
}
}
std::cout<<a[n].size()<<"\n";
while(a[n].size()){
for(auto x:a[n].front())std::cout<<x<<" ";
std::cout<<"\n";
a[n].pop();
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
//std::cin>>t;
while (t--) fc();
return 0;
}H. 可能是字符串签到题
题目大意
给定一个长度为
数据范围:
- 字符串
仅包含小写字母
思路
1. 问题性质转化(单字符支配性)
题目询问的是子串
假设某个长度为
由此可得:
即出现频次最高的子串必可退化为单个字符。原问题直接等价于:在子串
2. 前缀和优化
由于字符集大小
- 定义
表示前缀 中字符 的出现次数。 - 状态转移方程为:
- 对于每次给定的询问
,字符 在该区间内的频次可利用前缀和在 复杂度内计算:
- 遍历
个字符取最大值,即可在 的时间内回答单次询问。
复杂度分析
时间复杂度:
- 预处理阶段:构建前缀和数组需要扫描整个字符串并更新各字符的前缀和,耗时
。 - 查询阶段:单次查询枚举
个字母计算区间和,耗时 ;处理 次询问的总耗时为 。 - 总时间复杂度:
,其中 。代入 ,整体运算量约为 ,在 内可轻松通过。
空间复杂度:
- 维护大小为
的前缀和数组。当 时,该数组约占用 内存。 - 总空间复杂度:
,处于常规限制( 或 )的安全阈值内。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void fc() {
int n,q;std::cin>>n>>q;
std::string s;std::cin>>s;
std::vector<std::vector<int>>b(26,std::vector<int>(n+1));
for(int i=0;i<n;i++){
int c=s[i]-'a';
b[c][i+1]=1;
}
for(int i=0;i<26;i++){
for(int j=1;j<=n;j++)b[i][j]+=b[i][j-1];
}
while(q--){
int l,r;std::cin>>l>>r;
int mx=0;
for(int i=0;i<26;i++)mx=std::max(mx,b[i][r]-b[i][l-1]);
std::cout<<mx<<"\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
//std::cin>>t;
while (t--) fc();
return 0;
}I. 山海关老冰棍
题目大意
工厂中有
数据范围:
思路
1. 问题性质与单调性分析
设总耗时为
在时间
易知函数
- 当
增大时,各生产线能生产的冰棍数量 必然单调不减,因此总产出 随之单调不减; - 当
时, 。
原问题要求求解满足
2. 二分上界估计与设计
- 下界(
):若 ,显然产出为 ,故可行解的开区间下界可设为 。 - 上界(
):考虑最劣情况,即使仅由生产耗时最短的生产线单独完成全部 份订单,耗时至多为 。在 64 位有符号整型( long long,最大值约)表示范围内不会超出,直接取上界为足够大值(如 LLONG_MAX)亦可。
3. 判定函数(Check)实现
对于二分出的时间点
- 累加各生产线的产出
。 - 一旦累加和
,即可提前返回 true。这一步不仅实现了常数级剪枝,而且有效规避了多项累加可能导致的 64 位整型溢出风险。 - 遍历结束后若
,返回 false。
采用开区间
复杂度分析
时间复杂度:
- 判定函数
check需要遍历条生产线,单次复杂度为 。 - 二分搜索的值域跨度为
(或 ),二分迭代次数为 次。 - 总时间复杂度为
。代入 ,总运算量约为 ,在时限内耗时极低。
空间复杂度:
- 仅需线性数组存储
条生产线的耗时参数 。 - 总空间复杂度为
。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void fc() {
int n,m;
std::cin>>n>>m;
std::vector<ll>a(n);
for(auto &i:a)std::cin>>i;
auto check=[&](ll k){
ll s=0;
for(auto& x:a){
s+=k/x;
if(s>=m)return true;
}
return s>=m;
};
ll l=0,r=1e18;
while(l+1<r){
ll mid=l+((r-l)>>1);
if(check(mid))r=mid;
else l=mid;
}
std::cout<<r;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
//std::cin>>t;
while (t--) fc();
return 0;
}J. 海鲜大排档点生蚝
题目大意
秦皇岛的海鲜大排档中共有
对于第
- 单买:价格为
元/个; - 整打购买:价格为
元/打( 打包含 个)。
求在最优购买策略下最多可以购买的生蚝总数。若有多家摊位均可达到该最大数量,输出其中编号最小的摊位编号。
数据范围:
思路
1. 单个摊位的贪心策略分析
对于固定的摊位
根据性价比对比,可以分为两种情况:
- 情况一:
此时单买 个的成本不高于整打购买的价格,整打购买没有任何价格优势。因此完全无需整打购买,全部预算用于单买即可取得最优解:
- 情况二:
此时整打购买具有更低的均价,即 。最优策略应尽可能优先整打购买:
- 最大化购买整打数量:
,获得 个生蚝; - 剩余资金为
。由于 ,已无法再买整打,剩余资金全部用于单买,获得 个生蚝。
贪心最优性证明: 是否存在“少买一打,腾出资金用于单买能获得更多生蚝”的情况? 若退订
即退订
2. 全局最优维护
遍历全部 ans 以及对应的最优摊位编号 as。当且仅当当前摊位获得的数量严格大于历史最大值时更新答案,天然保证了在数量并列时保留编号更小的摊位。
复杂度分析
时间复杂度:
- 对于每家摊位,计算最大购买量仅涉及常数次基础整数运算(乘法、除法、取模),单次复杂度为
。 - 遍历所有
家摊位,整体时间复杂度为 。在 下耗时在数毫秒内。
空间复杂度:
- 算法采用在线扫描处理,无需持久化存储摊位数据,仅需常数级别的变量辅助,整体空间复杂度为
。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll f(ll a,ll b,ll m){
ll aa=a*12;
if(aa<=b)return m/a;
else{
ll res=m/b*12;
m%=b;
return res+m/a;
}
}
void fc() {
ll n,m;
std::cin>>n>>m;
ll ans=0,as=0;
for(int i=0;i<n;i++){
int a,b;std::cin>>a>>b;
ll k=f(a,b,m);
if(k>ans){
as=i+1;
ans=k;
}
}
std::cout<<ans<<" "<<as;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
//std::cin>>t;
while (t--) fc();
return 0;
}M. 拯救猫猫
题目大意
给定一个 .)、障碍物(#)、起点 U、D、L、R 分别表示上、下、左、右)。
每只狗的视线沿其正前方直线延伸,直到遇到障碍物或另一只狗为止。猫猫需要从起点
- 只能在上下左右相邻的四个空地间移动,不能走出网格;
- 不能进入任何狗的视线覆盖范围;
- 不能踏入障碍物或狗所在的格子。
现在可以选择恰好一只狗让其睡着。睡着的狗会失去其视线,但该位置本身依然不可通行。问是否存在一种方案使得猫猫能成功从 -1 -1。
数据范围:
,且所有测试数据中 与 的总和均不超过 - 起点
和出口 保证初始时不在任何狗的视线内,且网格中至少存在一只狗
思路
1. 问题建模与视线预处理
猫猫移动的限制核心在于“不能进入任何狗的视线”。设全网格中每只狗的视线路径为一个有向线段集合。
- 若一个空地被
只狗的视线重叠覆盖,则使其中任意一只狗睡着,该格子依然处于其他狗的监视之下,依然不可通行; - 若一个空地恰好只被
只狗的视线覆盖,则当且仅当这只特定的狗睡着时,该格子才会重新变为可行走的空地; - 若一个空地从未被任何狗覆盖(包括
和 ),则它始终是绝对安全的通行格。
因此,可以先遍历所有的狗,沿其朝向发射射线,在遇到边界、障碍物或其他狗之前,对路径上的空地统计覆盖次数(在代码中通过在字符 '1' 上递增模拟计数)。
2. 基底图连通性与动态扩展
对于初始状态下未被任何狗监视的格子集合
- 我们利用并查集(DSU)预先将所有四连通的初始安全格子合并,建立静态的连通分量基底。
- 此时若直接将某只狗置为睡眠状态,相当于将该狗视线上覆盖次数恰好为
的格子动态加入图。这些新解锁的格子需要:
- 与其自身射线方向上相邻的解锁格子连边;
- 与其上下左右相邻的初始安全格子连边。
3. 可撤销并查集(Rollback DSU)
网格中可能存在多只狗,若对每只狗都重新构建完整的并查集,时间开销将达到
注意到每次让一只狗睡着所带来的图结构变化是暂时的、局部的:
- 采用按秩/按大小合并(Union by Size)并不使用路径压缩的可撤销并查集。
- 在测试某只狗之前,记录并查集的操作历史版本快照(Snapshot)。
- 动态连边并判定起点
与出口 是否连通。 - 判定结束后,通过操作栈回滚(Rollback)撤销该狗引起的所有合并操作,瞬间还原到基底图状态。
4. 贪心搜索与字典序优先
按照从上到下、从左到右的顺序依次收集每只狗。由于题目要求优先输出行坐标最小、其次列坐标最小的方案,因此按该顺序枚举测试狗。一旦某只狗能使 -1 -1。
复杂度分析
时间复杂度:
- 视线覆盖预处理:每只狗的射线最长为
。设狗的总数为 ,射线遍历的总步数不超过 ,在最坏网格分布下受限于 。 - 初始基底合并:遍历所有单元格并合并相邻安全格,由于采用按大小合并的并查集,单次合并耗时
,总耗时 。 - 枚举与回滚:对于每只狗,其视线上解锁的格子数不超过射线长度
。每个解锁格至多进行 次合并操作,单次代价 ,回滚代价为 。因此每只狗的检验代价为 。 - 总时间复杂度:
。对于 ,整体运算量完全在时限( )允许范围内。
空间复杂度:
- 网格地图占用
内存; - 并查集数组
fa、siz占用内存; - 操作历史栈
his的深度在基底合并时至多为,在单只狗测试阶段至多增长 。 - 总空间复杂度:
。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct DSU {
std::vector<int> fa, siz;
struct op {
int u, v;
};
std::vector<op> his;
DSU(int n) : fa(n + 1), siz(n + 1, 1) { std::iota(fa.begin(), fa.end(), 0); }
int find(int x) {
while (x != fa[x]) x = fa[x];
return fa[x];
}
bool same(int x, int y) { return find(x) == find(y); }
bool merge(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return false;
if (siz[x] < siz[y]) std::swap(x, y);
siz[x] += siz[y];
fa[y] = x;
his.push_back({x, y});
return true;
}
int snapshot() const { return his.size(); }
void rollback(int snap) {
while ((int)his.size() > snap) {
auto& [u, v] = his.back();
siz[u] -= siz[v];
fa[v] = v;
his.pop_back();
}
}
};
bool isDog(char c) { return c == 'U' || c == 'D' || c == 'L' || c == 'R'; }
void Dxy(int& dx, int& dy, char c) {
if (c == 'U')
dx = -1, dy = 0;
else if (c == 'D')
dx = 1, dy = 0;
else if (c == 'L')
dx = 0, dy = -1;
else
dx = 0, dy = 1;
}
int dx[] = {1, -1, 0, 0};
int dy[] = {0, 0, 1, -1};
void fc() {
int n, m;
std::cin >> n >> m;
auto id = [&](int x, int y) { return x * m + y; };
std::vector<std::string> s(n);
for (int i = 0; i < n; i++) std::cin >> s[i];
std::vector<std::pair<int, char>> dog;
dog.reserve(2000);
auto isGo = [&](int x, int y, bool is) -> bool {
if (x < 0 || x >= n || y < 0 || y >= m) return false;
return s[x][y] == '.' || s[x][y] == 'S' || s[x][y] == 'E' || (is && s[x][y] == '1');
};
int start = -1, end = -1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (s[i][j] == 'S')
start = id(i, j);
else if (s[i][j] == 'E')
end = id(i, j);
else if (isDog(s[i][j]))
dog.push_back({id(i, j), s[i][j]});
}
}
for (auto& [idx, c] : dog) {
int dxx, dyy;
Dxy(dxx, dyy, c);
int x = idx / m + dxx, y = idx % m + dyy;
while (true) {
if (x < 0 || x >= n || y < 0 || y >= m || s[x][y] == '#'||isDog(s[x][y])) break;
if (s[x][y] == '.')
s[x][y] = '1';
else
s[x][y]++;
x += dxx;
y += dyy;
}
}
DSU dsu(n * m);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!isGo(i, j, 0)) continue;
for (int k = 0; k < 4; k++) {
int nx = i + dx[k], ny = j + dy[k];
if (isGo(nx, ny, 0)) {
dsu.merge(id(i, j), id(nx, ny));
}
}
}
}
for (auto& [idx, c] : dog) {
int dxx, dyy;
Dxy(dxx, dyy, c);
int x = idx / m + dxx, y = idx % m + dyy;
int snap = dsu.snapshot();
int prex = -1, prey = -1;
int k=0;
while (true) {
if (x < 0 || x >= n || y < 0 || y >= m || s[x][y] == '#' || isDog(s[x][y])) break;
if (s[x][y] != '1') {
prex = prey = -1;
x += dxx;
y += dyy;
continue;
}
if(prex!=-1)dsu.merge(id(x,y),id(prex,prey));
for (int i = 0; i < 4; i++) {
int nx = x + dx[i], ny = y + dy[i];
if (isGo(nx, ny, 0)) dsu.merge(id(x, y), id(nx, ny));
}
prex=x,prey=y;
x += dxx;
y += dyy;
}
if (dsu.same(start, end)) {
std::cout << idx / m + 1 << " " << idx % m + 1 << "\n";
return;
}
dsu.rollback(snap);
}
std::cout << "-1 -1" << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}