P9343 一曲新词酒一杯

记录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 张红纸。

问在至少几次操作后,每杯酒上至少有一张红纸?


思路

如果是操作一:单个酒杯操作

  1. 直接贴到酒杯上 👉 操作数组代表酒杯被贴纸
  2. 计数器记下来这是一共贴了几个酒杯

如果是操作二:全方位操作

  1. 如果当前这杯酒被贴过,直接让计数器等于酒杯数
  2. 如果这杯酒没被贴过,记录下这是第几次全方位操作
  3. 如果进行过两次全方位贴纸,计数器直接拉满
  4. 记录下这杯酒被进行过全方位操作

代码简析

#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中遇到多情况多状态数组记录的万能处理框架


一、核心思想:状态建模三步走

所有多状态问题,本质都是状态空间建模

  1. 识别状态维度:时间?空间?属性?操作?

  2. 选择数据结构:数组/结构体/map/bitset

  3. 设计状态转移:记录 → 查询 → 更新


二、七大万能处理办法(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);  // 前缀和优化
}

四、内存优化技巧(防爆炸)

  1. 数组大小计算N × sizeof(类型) < 256MB × 0.8

  2. 三维改二维:将 a[N][M][K] 拆分为 a[N][M] 的vector

  3. short代替int:值范围小时用short(2字节)

  4. bool代替char:标记数组用bool(1字节)

  5. 时间戳代替memset:多组数据用全局时间戳,O(1)清空


五、竞赛决策树(看到题先选结构)

题目描述 → 状态有几个维度?
    ├── 1维 → 一维数组
    ├── 2维 → 二维数组/vector
    ├── 3维+ → 结构体/降维/滚动数组
    │
是否需要动态扩展?
    ├── 是 → vector或map
    ├── 否 → 静态数组
    │
是否需要频繁区间查询?
    ├── 是 → 前缀和
    ├── 否 → 直接遍历
    │
状态值是否只0/1?
    ├── 是 → bitset
    ├── 否 → 普通数组

六、一句话总结

CSP-J多状态处理 = 先设计状态维度 → 选数据结构(数组优先,大而稀疏用map,0/1用bitset) → 预处理(前缀和) → 查询/更新(O(1)/O(log n)),记住:状态连续用数组,离散用map,能降维就降维,能滚动就滚动,内存计算要当心。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值