时间限制:C/C++ 2秒,其他语言4秒
空间限制:C/C++ 524288K,其他语言1048576K
64bit IO Format: %lld题目描述
大家在乘坐火车的时候,经常遇到售货员卖一些零食小吃。他们有时会把自己所售卖的物品名字编成押韵的顺口溜,比如“花生瓜子八宝粥,啤酒饮料矿泉水,腿收一下”,“花生瓜子矿泉水,啤酒饮料爆米花,德州扒鸡大碗面”这样做似乎可以让销量更好。
在一辆有n节车厢的火车上,车厢编号从前往后依次为[1,n][1,n][1,n]的整数。其中每个车厢都会有若干项需要的物品,物品的编号是范围在[1,m][1,m][1,m]的整数。
售货员Alice会被安排在火车的其中一段连续的车厢里叫卖商品。Alice是一名有文采且敬业的售货员,不管被分配到哪一段连续的车厢售货,她都会准备好那些车厢所需要的所有物品,并且把这些物品的名字编成一段顺口溜。Alice请让你帮忙计算一下:为了应对所有情况,她需要写多少段不同的顺口溜?输入描述:
第一行输入两个正整数 n,mn,mn,m满足(1≤n≤2×105)(1\le n\le 2\times 10^5)(1≤n≤2×105),(1≤m≤100)(1\leq m \leq 100)(1≤m≤100)。分别代表车厢节数和物品编号最大值。 其后n行,第i行首先是一个整数kik_iki,满足1≤ki≤m1\le k_i \le m1≤ki≤m,代表第i−1i-1i−1节车厢需要的物品种类数。其后kik_iki个整数,代表第i−1i-1i−1节车厢所需要的物品的编号。保证kik_iki的和不超过10610^6106,保证输入的物品编号是[1,m][1,m][1,m]内的整数,并且同一节车厢的物品编号互不相同。输出描述:
请你输出一行一个整数,代表Alice需要编写的顺口溜的个数。示例1
输入
复制5 5 1 1 2 1 2 2 4 5 1 1 2 2 3
5 5 1 1 2 1 2 2 4 5 1 1 2 2 3输出
复制8
8说明
以下列出所有区间的物品集合: 区间[1,1]:物品{1}, 区间[1,2]:物品{1,2}, 区间[1,3]:物品{1,2,4,5}, 区间[1,4]:物品{1,2,4,5}, 区间[1,5]:物品{1,2,3,4,5}, 区间[2,2]:物品{1,2},区间[2,3]:物品{1,2,4,5},区间[2,4]:物品{1,2,4,5},区间[2,5]:物品{1,2,3,4,5},区间[3,3]:物品{4,5},区间[3,4]:物品{1,4,5},区间[3,5]:物品{1,2,3,4,5},区间[4,4]:物品{1},区间[4,5]:物品{1,2,3},区间[5,5]:物品{2,3}。 其中出现的物品集合有: {1},{1,2},{1,2,4,5},{1,2,3,4,5},{4,5},{1,4,5},{1,2,3},{2,3}共8种
我的答案:超时
一、信息
- 题目描述:计算在一列火车上售货,需要准备的不同顺口溜的数量,考虑到每节车厢有不同的物品需求。
- 输入:
- n:车厢的数量。
- m:物品编号的最大值。
- 接下来的 n 行,每行代表一节车厢所需的物品种类和编号。
- 输出:需要编写的顺口溜的个数。
二、分析
- 每节车厢的物品需求定义了一个物品集合。
- 连续车厢的物品需求可以通过合并单个车厢的需求来获得。
- 顺口溜的数量等于所有可能的连续车厢组合中,不同物品集合的数量。
三、算法设计
- 预处理每节车厢的物品需求:将每节车厢的物品需求转换成一个集合(或等价的数据结构),以便快速合并和比较。
- 计算所有可能的连续车厢组合:通过遍历所有可能的起始和结束车厢,计算每一种组合的物品需求集合。
- 去重和计数:使用一个高效的数据结构(如哈希表)来存储不同的物品集合,以此来计数需要编写的顺口溜的数量。
四、代码实现(C++)
#include <iostream>
#include <vector>
#include <set>
#include <unordered_map>
#include <algorithm> // 包含<algorithm>头文件
using namespace std;
// 自定义哈希函数
struct vector_hash {
size_t operator()(const vector<int>& v) const {
hash<int> hasher;
size_t seed = 0;
for (int i : v) {
seed ^= hasher(i) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
return seed;
}
};
int main() {
int n, m;
cin >> n >> m;
vector<set<int>> cars(n);
for (int i = 0; i < n; ++i) {
int k;
cin >> k;
while (k--) {
int item;
cin >> item;
cars[i].insert(item);
}
}
unordered_map<vector<int>, bool, vector_hash> unique_combinations;
for (int i = 0; i < n; ++i) {
set<int> current_combination;
for (int j = i; j < n; ++j) {
current_combination.insert(cars[j].begin(), cars[j].end());
vector<int> combination_vector(current_combination.begin(), current_combination.end());
unique_combinations[combination_vector] = true;
}
}
cout << unique_combinations.size() << endl;
return 0;
}
五、实现代码过程中可能遇到的问题
- 性能问题:由于需要计算所有可能的连续车厢组合,这个过程在 n 很大时可能会非常慢。优化算法或者使用更高效的数据结构可能是必要的。
- 内存消耗:存储所有不同的物品组合可能会消耗大量内存,特别是当 n 和 m 较大时。可以考虑只存储组合的哈希值来减少内存使用。
- 去重复:确保每种物品组合只被计算一次。
正确答案:
一、信息
- 题目描述:在一列有 n 节车厢的火车上,每节车厢可能需要一些编号为 [1, m] 的物品。售货员 Alice 被分配到任意一段连续车厢售货时,需要准备好那些车厢所需的所有物品,并且编写顺口溜。任务是计算 Alice 需要编写的不同顺口溜的数量。
- 输入:
- n 和 m 分别代表车厢数量和物品编号的最大值。
- 接下来的 n 行,每行描述一节车厢所需要的物品种类数和具体的物品编号。
- 输出:一个整数,表示不同顺口溜的总数。
二、分析
- 核心问题:计算所有可能的连续车厢组合中,不同物品组合的数量。
- 挑战:直接枚举所有车厢组合的方法在数据量大时会导致超时。
三、算法设计
- 步骤:
- 预处理:用二维布尔数组
st
记录每节车厢是否需要每种物品。用一维数组id
记录每种物品最后出现的车厢位置。 - 下一个位置的预处理:对于每种物品和每节车厢,计算该物品在当前车厢之后的下一个出现位置,存储在
nex
数组中。 - 排序和去重:对于每节车厢,基于
nex
数组的信息,将物品按照下一个出现位置进行排序,然后使用128位整数(__int128_t
)通过位操作生成每种组合的唯一标识,利用set
进行去重。 - 统计不同顺口溜数量:
set
的大小即为所求的不同顺口溜的数量。
- 预处理:用二维布尔数组
四、代码实现(C++)
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
int n, m;
bool st[200005][105];
int id[105];
pii nex[200005][105];
set<__int128_t> mp;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int k;
cin >> k;
while (k--) {
int op;
cin >> op;
st[i][op] = true;
}
}
for (int i = 1; i <= m; i++) {
id[i] = 3e5;
}
for (int i = n; i >= 1; i--) {
for (int j = 1; j <= m; j++) {
if (st[i][j]) id[j] = i;
nex[i][j] = {id[j], j};
}
}
for (int i = 1; i <= n; i++) {
sort(nex[i]+1, nex[i]+1+m);
__int128_t con = 0;
for (int j = 1; j <= m; j++) {
if (nex[i][j].first == 3e5) break;
int op = nex[i][j].first;
while (nex[i][j].first == op && j <= m && nex[i][j].first != 3e5) {
con += ((__int128_t)1) << nex[i][j].second;
j++;
}
j--;
mp.insert(con);
}
}
cout << mp.size() << endl;
return 0;
}
五、实现代码过程中可能遇到的问题
- 性能优化:算法的关键在于通过高效的数据结构和位操作来处理大量数据,以避免运行超时。
六、复盘
我的答案为什么错了?
理解和处理大量数据的方法
-
预处理效率:预处理步骤(例如记录每种物品最后出现的车厢位置和计算每节车厢对于每种物品下一次出现的位置)需要高效地执行。如果这些步骤没有优化,即使是简单的遍历和更新操作也可能导致时间消耗过大。
-
排序和去重策略:排序每节车厢中物品的下一次出现位置和利用
set
去重的过程中,如果处理不当(比如没有合理利用数据结构的特性或者对数据的操作过于频繁),也可能导致算法效率低下。 -
位操作和集合大小:利用
__int128_t
进行位操作生成每种组合的唯一标识是一个高效的去重方法,但这要求对位操作和数据表示有深入的理解。如果处理不当,比如不恰当的位操作或不必要的重复计算,也可能增加算法的运行时间。
解决超时的关键策略
-
优化数据结构:选择合适的数据结构来存储和处理问题中的数据是避免超时的关键。例如,使用哈希表(如C++中的
unordered_map
)可以加快查找和更新操作的速度。 -
减少不必要的计算:通过仔细设计算法来避免不必要的计算,例如避免重复计算相同的物品组合或者预先排除那些显然不会产生新顺口溜的车厢组合。
-
使用高效的算法:比如,利用动态规划、贪心算法或其他高效算法思想来减少问题解决过程中的计算量。
对正确答案的评价:
1. 数据预处理
通过预处理每种物品最后出现的位置和为每节车厢计算物品的下一个出现位置,该方法有效减少了后续操作中的计算量。这种预处理是解决大规模数据问题的常见策略,它将问题复杂度从可能的组合数降低到了处理单个元素的操作上。
2. 位运算的应用
利用 __int128_t
类型通过位运算来唯一标识每种物品组合是该解决方案的亮点之一。这种方法极大地提高了去重效率,并且相比传统的集合或列表表示法,大大减少了内存使用。位运算在处理此类问题时是一个非常强大的工具,因为它能够以极高的空间和时间效率来表示和处理集合数据。
3. 排序与去重
使用排序和 set
来去重是解决此类问题的另一个有效策略。排序保证了处理的一致性和效率,而 set
的使用确保了每种物品组合只被计算一次。这种组合不仅提高了算法的效率,也保证了算法结果的正确性。
4. 效率与实用性
该解决方案在处理大规模数据集时显示出了很高的效率,它很好地平衡了计算速度和内存使用。此外,解决方案中使用的数据结构和算法思想都是非常实用的,可以被应用到其他类似的问题中。
5. 可读性和维护性
尽管解决方案高效且实用,但对于不熟悉位运算或高级数据结构的开发者来说,代码的可读性和维护性可能是一个挑战。解决方案中的一些高级技巧(如 __int128_t
的使用和位操作)需要一定的学习和理解。
总结
正确答案展示了一种高效解决复杂问题的方法,它利用了数据预处理、位运算、排序和去重等技术。这种方法不仅解决了问题,也提供了一种对于处理类似问题的通用框架。然而,为了保证代码的可读性和维护性,可能需要对其中的一些高级技巧进行适当的封装和注释。