记录55
#include<bits/stdc++.h>
using namespace std;
bool a[200010]={},vis[200010]={};
int main(){
int T,n,m,t,w;
cin>>T;
while(T--){
cin>>n>>m;
memset(a,0,sizeof(a));
memset(vis,0,sizeof(vis));
int ans=0,cnt=0,cntV=0;
while(m--){
cin>>t>>w;
if(cnt==n) continue;
ans++;
if(t==1){
if(a[w]==0){
cnt++;
a[w]=1;
if(vis[w]==1) cnt=n;
}
}
if(t==2){
if(a[w]==1) cnt=n;
if(vis[w]==0) cntV++;
if(cntV==2) cnt=n;
vis[w]=1;
}
if(cnt==n) cout<<ans<<endl;
}
if(cnt!=n) cout<<-1<<endl;
}
return 0;
}
题目传送门
https://www.luogu.com.cn/problem/P9343
突破点
接下来对这 n 杯酒依次进行 m 次操作。
操作共分为 2 种:
1 x:给 x 号酒贴上 1 张红纸。2 x:给除了 x 号酒的其它 n−1 杯酒分别贴上 1 张红纸。问在至少几次操作后,每杯酒上至少有一张红纸?
思路
如果是操作一:单个酒杯操作
- 直接贴到酒杯上 👉 操作数组代表酒杯被贴纸
- 计数器记下来这是一共贴了几个酒杯
如果是操作二:全方位操作
- 如果当前这杯酒被贴过,直接让计数器等于酒杯数
- 如果这杯酒没被贴过,记录下这是第几次全方位操作
- 如果进行过两次全方位贴纸,计数器直接拉满
- 记录下这杯酒被进行过全方位操作
代码简析
#include<bits/stdc++.h>
using namespace std;
bool a[200010]={},vis[200010]={};
int main(){
int T,n,m,t,w;
cin>>T;
while(T--){
cin>>n>>m;
memset(a,0,sizeof(a));
memset(vis,0,sizeof(vis));
int ans=0,cnt=0,cntV=0;
while(m--){
...
return 0;
}
bool a[200010]={}(酒杯被贴纸情况),vis[200010]={};(进行全方位贴纸情况统计)
int T(测试数据组数),n(共几杯酒),m( 共几 次操作),t(第几种操作),w(第几个酒水杯);
memset() 👉 每组数据开始前,进行初始化
int ans=0(操作次数),cnt=0(统计被贴酒杯数),cntV=0(全方位操作次数记录);
#include<bits/stdc++.h>
using namespace std;
bool a[200010]={},vis[200010]={};
int main(){
int T,n,m,t,w;
cin>>T;
while(T--){
cin>>n>>m;
memset(a,0,sizeof(a));
memset(vis,0,sizeof(vis));
int ans=0,cnt=0,cntV=0;
while(m--){
cin>>t>>w;
if(cnt==n) continue;
ans++;
if(t==1){
if(a[w]==0){
cnt++;
a[w]=1;
if(vis[w]==1) cnt=n;
}
}
if(t==2){
....
}
return 0;
}
if(cnt==n) continue; 👉 如果够了不需要就行后面的操作,已经全贴了
注意:不能用break跳出循环,因为这样会不能接收后面的输入数据
ans++; 👉 管你这那的,记录个数先
if(t==1){} 👉 如果是操作一
if(a[w]==0){} 👉 如果酒杯没贴过
cnt++; 👉 记录下贴了的酒杯数
a[w]=1; 👉 这杯酒贴过了
if(vis[w]==1) cnt=n; 👉 如果之前对这杯酒进行过全方位操作,计数器直接拉满
if(t==2){} 👉 如果是操作二
#include<bits/stdc++.h>
using namespace std;
bool a[200010]={},vis[200010]={};
int main(){
int T,n,m,t,w;
cin>>T;
while(T--){
cin>>n>>m;
memset(a,0,sizeof(a));
memset(vis,0,sizeof(vis));
int ans=0,cnt=0,cntV=0;
while(m--){
cin>>t>>w;
if(cnt==n) continue;
ans++;
if(t==1){
if(a[w]==0){
cnt++;
a[w]=1;
if(vis[w]==1) cnt=n;
}
}
if(t==2){
if(a[w]==1) cnt=n;
if(vis[w]==0) cntV++;
if(cntV==2) cnt=n;
vis[w]=1;
}
if(cnt==n) cout<<ans<<endl;
}
if(cnt!=n) cout<<-1<<endl;
}
return 0;
}
if(a[w]==1) cnt=n; 👉 如过进行全方位操作的酒杯,被贴过纸,直接拉满计数器
if(vis[w]==0) cntV++; 👉 这杯酒没进行过全方位操作,直接记录一次全方位操作
if(cntV==2) cnt=n; 👉 两次不同酒杯全方位操作后,所有酒杯全部被贴
if(cnt==n) cout<<ans<<endl; 👉 每次操作后,看看是不是满足全酒杯被贴
if(cnt!=n) cout<<-1<<endl; 👉 循环m次操作结束后,没有把全酒杯操作,输出-1
补充
CSP-J中遇到多情况多状态数组记录的万能处理框架
一、核心思想:状态建模三步走
所有多状态问题,本质都是状态空间建模:
识别状态维度:时间?空间?属性?操作?
选择数据结构:数组/结构体/map/bitset
设计状态转移:记录 → 查询 → 更新
二、七大万能处理办法(CSP-J全覆盖)
办法1:多维数组(时空维度)
场景:网格、时间+属性、两两关系
// 例:记录每个班每个学生的分数 int score[11][51]; // [班][学生] // 例:记录每天每小时的温度 int temp[31][24]; // [日][小时] // 查询3班所有学生总分 int sum = 0; for (int i = 1; i <= 50; i++) sum += score[3][i];优势:访问O(1),缓存友好
陷阱:三维以上谨慎,内存爆炸(int[500][500][500]= 500MB)
办法2:结构体数组(属性维度)
场景:一个实体多个属性(学生、商品)
struct Student { int id, chinese, math; ll total; // 总分必须long long } stu[1005]; // 记录 stu[i].total = stu[i].chinese + stu[i].math; // 按总分排序(贪心/DP常用) sort(stu + 1, stu + n + 1, [](auto &a, auto &b){ return a.total > b.total; });优势:语义清晰,可维护性强
陷阱:对齐填充浪费内存(可重排成员减少)
办法3:map/unordered_map(稀疏状态)
场景:坐标值大但有效点少(离散化)
// 例:记录坐标(1e9)出现的次数 map<int, int> cnt; // 坐标 → 次数 cnt[x]++; // O(log n) // 例:邻接表(节点多但边少) vector<int> g[N]; // 推荐用vector的vector优势:省内存,下标任意
注意:O(log n)访问,比数组慢,10⁶次内可用
办法4:前缀和/差分数组(区间查询/修改)
场景:频繁求子段和、区间加
// 记录每天销售额 ll pre[N]; // 前缀和 for (int i = 1; i <= n; i++) pre[i] = pre[i-1] + a[i]; // 查询[l,r]区间和 O(1) ll sum = pre[r] - pre[l-1]; // 区间加(差分) diff[l] += x; diff[r+1] -= x; for (int i = 1; i <= n; i++) a[i] = a[i-1] + diff[i];优势:查询O(1),竞赛必备
注意:预处理O(n),只能静态
办法5:bitset/bitmask(0/1状态压缩)
场景:访问标记、状态压缩DP
bitset<N> vis; // 记录N个位置是否访问 vis.set(5); // 标记5已访问 if (vis.test(10)) { ... } // 查询10 // 或手动掩码 int mask[32]; // 每个int存32个0/1状态 int idx = i / 32, bit = i % 32; mask[idx] |= (1 << bit); // 置1优势:极度省空间(32倍),位运算快
劣势:只存0/1,操作稍复杂
办法6:时间戳数组(多轮遍历)
场景:避免重复访问,替代memset清零
int vis[N]; // 访问标记 int global_time = 0; // 全局计时器 // 第一轮遍历 global_time++; for (int i = 1; i <= n; i++) { if (vis[i] != global_time) { // 本轮未访问 vis[i] = global_time; dfs(i); } } // 第二轮遍历前,时间+1,自动清空 global_time++; // vis数组全部"清空"优势:O(1)批量清空,比memset快
用途:多组测试数据,避免重复初始化
办法7:滚动数组(降维优化)
场景:DP只依赖前一层状态
// dp[i][j] = f(dp[i-1][j], dp[i][j-1]) ll dp[2][N]; // 只用两行 int cur = 0, nxt = 1; for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { dp[nxt][j] = max(dp[cur][j], dp[cur][j-1]) + a[i][j]; } swap(cur, nxt); // 滚动 }优势:空间从O(n²)降到O(n)
注意:需仔细设计滚动方向,避免数据覆盖
三、多状态融合处理(CSP-J压轴题)
融合1:结构体+排序(贪心)
struct Node { int id, val, type; } nodes[N]; // 先按类型分组,组内按值降序 sort(nodes, nodes + n, [](auto &a, auto &b){ if (a.type != b.type) return a.type < b.type; return a.val > b.val; }); // 遍历处理,相同类型批量操作 for (int i = 0; i < n; ) { int j = i; while (j < n && nodes[j].type == nodes[i].type) j++; // 处理[i, j)区间,同一类型 process(i, j); i = j; }融合2:map + vector(邻接表)
// 场景:每个节点有多个属性邻居 unordered_map<int, vector<pair<int, int>>> g; // g[u] = [(v1, w1), (v2, w2), ...] g[u].push_back({v, w}); // 加边 for (auto &p : g[u]) { int v = p.first, w = p.second; }融合3:前缀和+优先队列(反悔贪心)
// 场景:选任务,总时间有限 priority_queue<int> pq; // 存已选任务时间 ll sum = 0, ans = 0; for (int i = 1; i <= n; i++) { pq.push(task[i].time); sum += task[i].time; if (sum > limit) { // 超限制 sum -= pq.top(); pq.pop(); // 踢掉最大时间 } ans = max(ans, pre[i] - sum); // 前缀和优化 }
四、内存优化技巧(防爆炸)
数组大小计算:
N × sizeof(类型) < 256MB × 0.8三维改二维:将
a[N][M][K]拆分为a[N][M]的vectorshort代替int:值范围小时用
short(2字节)bool代替char:标记数组用
bool(1字节)时间戳代替memset:多组数据用全局时间戳,O(1)清空
五、竞赛决策树(看到题先选结构)
题目描述 → 状态有几个维度? ├── 1维 → 一维数组 ├── 2维 → 二维数组/vector ├── 3维+ → 结构体/降维/滚动数组 │ 是否需要动态扩展? ├── 是 → vector或map ├── 否 → 静态数组 │ 是否需要频繁区间查询? ├── 是 → 前缀和 ├── 否 → 直接遍历 │ 状态值是否只0/1? ├── 是 → bitset ├── 否 → 普通数组
六、一句话总结
CSP-J多状态处理 = 先设计状态维度 → 选数据结构(数组优先,大而稀疏用map,0/1用bitset) → 预处理(前缀和) → 查询/更新(O(1)/O(log n)),记住:状态连续用数组,离散用map,能降维就降维,能滚动就滚动,内存计算要当心。
5447

被折叠的 条评论
为什么被折叠?



