#include <iostream> #include <vector> #define int long long using namespace std; const int N = 3e6 + 5; signed main() { ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int t; cin >> t; while(t --) { int n, m; cin >> n >> m; vector<vector<int>> trie(N, vector<int>(65, 0)); vector<int> cnt(N, 0); int idx = 0; auto get = [&](char c) -> int{ if(c >= 'A' && c <= 'Z') return c - 'A'; else if(c >= 'a' && c <= 'z') return c - 'a' + 26; return c - '0' + 52; }; auto insert = [&](string s) -> void{ int p = 0; for(int i = 0; i < s.size(); i ++) { int x = get(s[i]); if(!trie[p][x]) { trie[p][x] = ++ idx; } p = trie[p][x]; cnt[p] ++; } }; auto find = [&](string s) -> int{ int p = 0; for(int i = 0; i < s.size(); i ++) { int x = get(s[i]); if(!trie[p][x]) return 0; p = trie[p][x]; } return cnt[p]; }; for(int i = 0; i < n; i ++) { string s; cin >> s; insert(s); } while(m --) { string s; cin >> s; cout << find(s) << '\n'; } } }这段代码MIE了,怎么优化
时间: 2025-07-14 07:40:17 浏览: 6
<think>嗯,我现在需要帮用户优化这段C++代码,因为它出现了内存超限(MIE)的问题。首先,我得仔细看一下代码的结构,找出哪里可能消耗过多的内存。
用户代码使用了两个大数组:trie树和cnt数组,都是大小为N(3e6+5)的vector。每个trie节点有65个子节点,所以总内存大约是3e6 * 65 * sizeof(int)。假设一个int是8字节(因为用了#define int long long),那大概是3e6 *65*8=1.56e9字节,也就是大约1.5GB左右,这显然超过了大部分OJ的内存限制,导致MIE。
所以问题出在静态预分配的trie结构上。原来的做法是为每个节点预先分配了65个子节点的空间,但实际情况下,很多节点并没有那么多子节点,这样会浪费大量内存。正确的优化方向应该是将trie结构改为动态分配,每个节点只在需要的时候创建子节点。
通常,Trie树的动态实现可以用结构体或类,每个节点包含一个哈希表或数组来存储子节点。但数组的话,如果每个节点动态创建大小为65的数组,仍然可能占用过多内存。因此,可能需要使用更紧凑的结构,比如用unordered_map或者用指针动态分配。
不过,考虑到性能问题(因为题目可能有大量数据),使用unordered_map可能会增加时间开销。这时候可以考虑将每个节点的子节点用更紧凑的方式存储,比如使用vector或者指针数组,但动态创建。
不过,在C++中,如果用指针的话,每个节点需要一个数组,每个元素是指向子节点的指针。例如,每个节点有一个长度为65的指针数组,但初始时为nullptr,只有在插入的时候才创建新的节点。这样,每个节点的子节点数组实际占用的内存是根据插入的字符动态分配的,而不是预先分配全部空间。
原来的代码中,trie是使用vector<vector<int>>,其中每个节点预先分配了65个int,导致内存爆炸。改为动态分配的话,每个节点只有在需要的时候才会创建子节点。例如,使用结构体Node,每个Node包含一个长度为65的指针数组,初始时都为nullptr。当需要添加子节点时,才new一个新的Node,并指向对应的位置。
另外,cnt数组也需要调整,改为每个节点保存一个计数,这样可以将cnt合并到Node结构体中,减少空间。
所以,优化步骤大概是:
1. 将trie的结构从预先分配的二维vector改为动态创建的节点结构体,每个节点包含子节点指针数组和计数。
2. 修改insert和find函数,使用指针操作,动态创建子节点。
这样,每个节点在初始化时不会占用65个int的空间,而是随着插入的字符串动态扩展,节省大量内存。
此外,原来的代码中,每次测试用例都会重新创建trie和cnt数组,但由于N是3e6+5,这会导致每个测试用例都占用大量内存。如果测试用例很多,可能也会有问题。但根据代码结构,每次处理一个测试用例时,都会重新初始化trie和cnt,所以可能问题出在单个测试用例的处理上。
所以,优化后的结构体应该为每个测试用例单独创建,并在处理完后释放内存,避免内存泄漏。或者,在C++中,可以使用智能指针,但可能比较复杂。不过,对于算法竞赛来说,可能更倾向于用动态节点管理,但要注意释放内存,否则会有内存泄漏。不过,在OJ中可能每个测试用例运行后,程序结束,所以可能不需要手动释放,但为了正确性,最好处理。
现在,具体如何实现呢?
结构体可能像这样:
struct Node {
int cnt = 0;
Node* children[65] = {nullptr};
};
然后,根节点是Node* root = new Node();
插入时,遍历字符串的每个字符,对于每个字符转换为索引x,检查当前节点的children[x]是否存在,不存在则创建。然后移动到该子节点,并增加cnt。
查找的时候,同样遍历字符,如果中途某个children[x]不存在,则返回0,否则到达最后节点,返回cnt。
但这样每个节点的大小是65个指针(每个8字节,假设64位系统)加上一个int,总共大约是65*8 +4= 524字节。但每个节点只有在实际存在子节点的时候才会分配空间。这样,总节点数如果是3e6的话,每个节点524字节的话,可能还是比较大?或者可能我的计算有问题?
例如,如果有N个节点,每个节点结构体的大小是524字节,那么3e6个节点就是3e6*524 ≈ 1.5e9字节,也就是约1.5GB,仍然可能超过内存限制。这可能还是有问题。
这说明这样的结构体可能还不够优化。那有没有更节省内存的方法?
另一种方式是,不使用固定大小的数组,而使用unordered_map来存储子节点。例如,每个节点有一个unordered_map<int, Node*> children,这样只有实际存在的子节点才会占用空间。但是unordered_map会有一定的开销,每个节点的哈希表可能需要更多的内存,但可能比固定数组更节省,尤其是当子节点数量较少的时候。
例如,如果大多数节点的子节点数量很少,比如平均每个节点只有几个子节点,那么用unordered_map存储会更省内存。但查找时间可能会稍微增加,但对于大部分情况来说,这可能可以接受。
所以,结构体改为:
struct Node {
int cnt = 0;
unordered_map<int, Node*> children;
};
这样,每个节点的children只保存实际存在的子节点。插入时,对于每个字符x,检查children[x]是否存在,不存在则创建。这样,每个节点的内存占用更小,尤其是当子节点数量少的时候。
但是,这样的实现可能在时间效率上不如数组,因为哈希表的查找是O(1)但常数较大。但根据题目要求,如果时间允许的话,这可能是一个可行的优化方向。
或者,可以尝试用更紧凑的结构,比如将子节点存储为一个动态数组,并用线性搜索,但这样时间会变得很慢,可能不适用于大量数据。
另一种优化思路是,将原来的二维数组trie和cnt数组从vector改为动态分配,但每个节点的子节点数组在需要时才分配。例如,原来的trie是一个二维数组,其中trie[p][x]表示节点p的第x个子节点。但原来的代码预分配了所有可能的节点,导致内存过大。如果改为每个节点的子节点数组动态分配,只有当该节点被访问到时才分配,可能可以节省内存。
或者,使用指针数组的链表结构。例如,每个节点有一个指针数组,初始为nullptr,当需要时才创建。
这可能与前面的结构体方法类似。原来的代码中的trie结构是预先分配所有节点的子节点数组,这非常浪费内存。如果改为每个节点仅在需要时创建子节点,那么可以节省大量内存。
例如,原来的代码中,vector<vector<int>> trie(N, vector<int>(65, 0)),这实际上是预先为每个节点p的每个可能的x(0-64)都分配了一个位置。但实际上,很多节点并没有这么多的子节点。例如,一个节点可能只有几个子节点,而其他都是0。所以,大部分空间被浪费了。
所以,正确的做法是将trie的结构改为动态的,每个节点的子节点数组只有在需要的时候才会被创建。
因此,可以将trie的结构改为一个动态的数组,每个节点保存一个map或者指针数组,初始时为空,插入时才分配。
在C++中,这可能用结构体来实现。例如:
struct TrieNode {
int cnt;
TrieNode* children[65]; // 或者用unordered_map<int, TrieNode*>
TrieNode() : cnt(0) {
memset(children, 0, sizeof(children)); // 初始化为nullptr
}
};
然后,根节点是TrieNode* root = new TrieNode();
在插入时,对于每个字符x,检查当前节点的children[x]是否存在。如果不存在,就创建一个新的TrieNode,并指向它。然后移动到该节点,并增加cnt。
这样,每个节点的子节点数组只有在实际使用时才会被分配空间,而不是预先分配。
这样的结构下,每个节点的大小是固定的:一个int cnt和65个指针。每个指针在64位系统下是8字节,所以每个节点的大小是4(假设int是4字节)+ 65*8 = 524字节。但是如果大部分节点的children数组大部分是nullptr,那么实际内存使用取决于插入的字符串数量和结构。
比如,如果有n个插入的字符串,平均每个字符串长度为k,那么总节点数大约是O(nk),所以总内存是O(nk * 524)。如果nk是3e6的话,总内存是3e6 * 524 ≈ 1.5e9字节,约1.5GB。这仍然可能超过内存限制,因为OJ通常内存限制是几百MB,比如256MB或512MB。这样看来,这可能还不够优化。
那有没有办法进一步减少每个节点的内存占用?
是的。例如,每个节点中的children数组可以改用更小的数据类型。原来的代码中使用int类型来存储子节点的索引,这可能占用了4或8字节。如果改为使用较小的类型,比如使用char或short来存储索引,但这可能不适用,因为节点数可能超过这些类型的范围。或者,可以将children数组中的指针替换为索引,但需要动态管理内存池。
或者,改用vector来存储子节点。例如,每个节点的子节点是一个vector<pair<int, TrieNode*>>,其中第一个元素是字符对应的索引,第二个是指针。这样,查找时需要遍历vector,时间复杂度为O(65)或者更高,但对于大量数据来说,这可能变慢。
或者,使用平衡二叉树结构,如map,但同样时间可能增加。
这时候,可能需要权衡时间和空间。如果时间允许,使用unordered_map来存储子节点可能更省空间,因为每个节点只保存实际存在的子节点。
例如,结构体改为:
struct TrieNode {
int cnt = 0;
unordered_map<int, TrieNode*> children;
};
这样,每个节点的children的大小仅取决于实际存在的子节点数量。比如,每个子节点在unordered_map中存储为一个键值对,键是int(x),值是指针。假设每个键值对的内存开销是较大的,比如每个条目需要存储hash表的相关信息,但总体来说,可能比固定数组更省内存,尤其是当每个节点的子节点数量较少时。
例如,如果有3e6个节点,每个节点的children平均有5个条目,那么每个unordered_map的内存占用可能比固定数组的65个指针小很多。比如,每个unordered_map的每个条目可能需要约32字节(假设),那么每个节点的children占用约5*32=160字节,加上cnt的4字节,结构体总大小约164字节。这样,3e6节点的话是3e6*164≈492MB,这可能在OJ的内存限制内。
当然,具体的实现和内存消耗可能因编译器和平台而异,但这样的优化方向是正确的。
所以,修改代码的结构,将trie树改为动态节点,每个节点使用unordered_map存储子节点,可以显著减少内存使用。
现在,回到代码的具体修改:
原代码中的trie是vector<vector<int>>,而cnt是vector<int>。每个节点在trie中的索引p对应cnt中的p的位置。现在,改用结构体动态管理,每个节点有自己的cnt,并且子节点通过unordered_map存储。
所以,在插入时,从根节点开始,逐字符处理,动态创建子节点。查找时同样处理。
此外,原代码中每次测试用例都会重新初始化trie和cnt,所以动态分配的结构需要在每个测试用例处理完毕后释放内存,否则会导致内存泄漏。但由于OJ可能每个测试用例单独运行,或者程序结束后自动释放,可能不需要处理,但为了正确性,应该在每个测试用例处理完后释放所有动态分配的内存。
但释放Trie树的内存可能需要递归删除所有子节点,这可能比较麻烦。或者,可以将每个测试用例的根节点创建为局部变量,在处理完该测试用例后,自动销毁根节点,并递归释放其所有子节点。
在C++中,可以编写一个辅助函数来递归删除节点:
void deleteTrie(TrieNode* node) {
if (!node) return;
for (auto& pair : node->children) {
deleteTrie(pair.second);
}
delete node;
}
然后,在每个测试用例处理完成后,调用deleteTrie(root)来释放内存。
不过,这可能增加代码的复杂度,但如果问题中的测试用例较多,可能导致内存不足。所以,必须确保每个测试用例处理完后,释放该用例的trie树。
综上,修改后的代码大致如下:
将原来的vector<vector<int>> trie和vector<int> cnt替换为结构体TrieNode,每个节点包含cnt和unordered_map<int, TrieNode*> children。
同时,修改insert和find函数,使用指针操作,动态创建节点。
另外,原来的get函数将字符转换为索引是正确的,这部分可以保留。
现在,根据这些分析,给出优化后的代码:
#include <iostream>
#include <vector>
#include <unordered_map>
#define int long long
using namespace std;
struct TrieNode {
int cnt = 0;
unordered_map<int, TrieNode*> children;
};
void deleteTrie(TrieNode* node) {
if (!node) return;
for (auto& pair : node->children) {
deleteTrie(pair.second);
}
delete node;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while (t --)
{
int n, m;
cin >> n >> m;
TrieNode* root = new TrieNode();
auto get = [&](char c) -> int {
if (c >= 'A' && c <= 'Z') return c - 'A';
else if (c >= 'a' && c <= 'z') return c - 'a' + 26;
return c - '0' + 52;
};
auto insert = [&](string s) -> void {
TrieNode* p = root;
for (char ch : s) {
int x = get(ch);
if (p->children.find(x) == p->children.end()) {
p->children[x] = new TrieNode();
}
p = p->children[x];
p->cnt ++;
}
};
auto find = [&](string s) -> int {
TrieNode* p = root;
for (char ch : s) {
int x = get(ch);
auto it = p->children.find(x);
if (it == p->children.end()) {
return 0;
}
p = it->second;
}
return p->cnt;
};
for (int i = 0; i < n; i ++) {
string s;
cin >> s;
insert(s);
}
while (m --) {
string s;
cin >> s;
cout << find(s) << '\n';
}
deleteTrie(root);
}
}
这样修改后,每个测试用例的trie树在处理完后会被完全释放,内存使用量取决于实际插入的节点数量,而不会预先分配大量内存,从而避免了MIE的问题。
此外,需要注意,unordered_map在某些情况下可能会有较高的内存开销,如果仍然出现内存问题,可以考虑改用其他结构,例如使用数组但动态分配,或者优化存储方式。但一般情况下,这样的改动应该能解决内存问题。
可能的进一步优化:
1. 使用数组指针的动态分配,但仅在需要时创建子节点。例如,每个节点的子节点数组是一个指针数组,初始为nullptr,需要时new。这可能会比unordered_map更节省内存,但每个节点仍需存储65个指针,不过在实际使用中,只有部分被分配。这可能比unordered_map更高效,但需要权衡。
例如,结构体改为:
struct TrieNode {
int cnt = 0;
TrieNode* children[65] = {}; // 初始化为nullptr
};
插入时:
if (!p->children[x]) {
p->children[x] = new TrieNode();
}
这可能比unordered_map更节省内存,因为每个节点的children数组在内存中是连续的,指针数组本身占用固定65*8=520字节,加上cnt的4字节,共524字节。但每个节点都必须分配这520字节,无论是否有子节点。而使用unordered_map的话,每个节点的children的大小根据实际子节点数量动态变化。因此,在子节点较多的情况下,数组方式更高效;在子节点较少的情况下,unordered_map更省内存。
例如,如果每个节点平均有5个子节点,那么使用数组方式每个节点需要524字节,而使用unordered_map每个节点可能需要(假设每个entry占32字节)5*32 + 4 = 164字节左右。这种情况下,unordered_map更省内存。但如果大部分节点有接近65个子节点,那么数组方式更优。
因此,如果题目中的字符串集合导致Trie树节点有很多子节点,那么数组方式可能更合适。否则,unordered_map更好。
回到问题,原代码的MIE是由于预分配了所有节点的子节点数组,所以改用动态分配的数组方式可能仍然导致每个节点占用较多内存。例如,如果有1e5个节点,每个节点524字节,总内存约52MB,这应该没问题。但如果节点数达到3e6,3e6*524≈1.5e9字节,约1.5GB,这仍然会超出内存限制。
那这说明原来的问题可能在于节点数过多,必须减少节点数。或者,可能原题的输入数据导致节点数太大,需要进一步优化。
这时候,可能需要重新审视问题,是否可以采用其他数据结构,如哈希表来替代Trie树。例如,将每个字符串插入哈希表,然后对于查询的前缀,统计有多少个字符串以该前缀开头。但哈希表无法直接高效地处理前缀查询,因此可能需要其他方法。
另一个思路是,使用前缀哈希的方法,将所有前缀存储起来,并用哈希表统计每个前缀的出现次数。例如,对于每个字符串s,生成其所有前缀,并将这些前缀存入哈希表,次数累加。但这样对于每个长度为k的字符串,需要生成k个前缀,总时间复杂度为O(nk),当n和k较大时可能无法承受。
例如,如果每个字符串平均长度为1e3,n是1e5,那么总共有1e8个前缀,这会导致时间和内存都不允许。
所以,这种情况下,Trie树仍然是更优的选择。
回到原问题,可能用户提供的代码中,将trie预分配为3e6+5的节点,每个节点65个int,导致内存过大。因此,必须采用动态分配方式。
所以,回到之前的优化方案,使用动态节点结构体,每个节点用unordered_map保存子节点,这样内存使用更灵活。
在测试时,可能需要注意,将结构体中的cnt的类型是否为int,原代码中的cnt是vector<int>,而用户将int定义为long long。所以,在结构体中,cnt的类型应为long long,而原代码中的vector<int> cnt(N, 0)可能错误,应该为vector<long long>。但在优化后的代码中,结构体的cnt的类型是int,这可能导致错误,因为原代码的find函数返回的是cnt[p],即int,而用户可能希望是long long。
在用户代码中,使用了#define int long long,所以所有int被替换为long long。因此,结构体中的cnt应该为long long类型。需要修正这一点,因为原代码中的cnt是vector<int>,但被替换为long long。
所以在结构体定义中,cnt应为long long:
struct TrieNode {
long long cnt = 0;
unordered_map<int, TrieNode*> children;
};
这样才能正确统计较大的数值。
同时,在插入和查找时,返回的cnt也是long long类型。
修改后的代码中,结构体中的cnt是long long类型,因此不会溢出。
综上,最终的优化代码应该将结构体的cnt类型改为long long,并正确使用动态节点管理,从而避免内存超限。
可能的最终代码如下:
#include <iostream>
#include <vector>
#include <unordered_map>
#define int long long
using namespace std;
struct TrieNode {
int cnt = 0;
unordered_map<int, TrieNode*> children;
};
void deleteTrie(TrieNode* node) {
if (!node) return;
for (auto& pair : node->children) {
deleteTrie(pair.second);
}
delete node;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while (t --)
{
int n, m;
cin >> n >> m;
TrieNode* root = new TrieNode();
auto get = [&](char c) -> int {
if (c >= 'A' && c <= 'Z') return c - 'A';
else if (c >= 'a' && c <= 'z') return c - 'a' + 26;
return c - '0' + 52;
};
auto insert = [&](string s) -> void {
TrieNode* p = root;
for (char ch : s) {
int x = get(ch);
if (p->children.find(x) == p->children.end()) {
p->children[x] = new TrieNode();
}
p = p->children[x];
p->cnt ++;
}
};
auto find = [&](string s) -> int {
TrieNode* p = root;
for (char ch : s) {
int x = get(ch);
auto it = p->children.find(x);
if (it == p->children.end()) {
return 0;
}
p = it->second;
}
return p->cnt;
};
for (int i = 0; i < n; i ++) {
string s;
cin >> s;
insert(s);
}
while (m --) {
string s;
cin >> s;
cout << find(s) << '\n';
}
deleteTrie(root);
}
}
这样修改后,每个测试用例的Trie树在结束后都会被释放,内存使用量大幅减少,避免了MIE的问题。同时,结构体中cnt的类型是int,但由于用户代码使用了#define int long long,这会替换为long long,所以结构体中的cnt实际上是long long类型。或者,需要显式将结构体的cnt声明为long long:
struct TrieNode {
long long cnt = 0;
unordered_map<int, TrieNode*> children;
};
否则,当用户代码中#define int long long时,原结构体中的int会被替换,但结构体中的成员名称可能与宏冲突。例如,原结构体中的int cnt会被替换为long long cnt,这没问题。或者,如果用户代码中使用了#define int long long,这可能导致结构体中的int被替换,这可能需要特别注意。
但根据用户提供的代码,他们使用了#define int long long,所以在结构体中的int会被替换为long long。因此,正确的结构体定义应为:
struct TrieNode {
int cnt = 0; // 这里由于宏替换,实际是long long cnt
unordered_map<int, TrieNode*> children;
};
因为用户代码中的#define int long long将所有的int替换为long long,包括结构体中的int。因此,结构体中的cnt的类型是long long,而不是int。因此,原代码中的结构体定义是正确的。
综上所述,最终的优化代码如上所述,通过动态分配节点,并使用unordered_map来存储子节点,从而大幅减少内存消耗,避免MIE错误。</think>该代码由于预分配了过大的Trie树导致内存超限。以下是优化思路及修改后的代码:
### 问题分析
原代码使用二维数组`trie[N][65]`预存储字典树,其中`N = 3e6+5`。每个节点占用65个int空间,总内存为:
$$3\text{e}6 \times 65 \times 8\text{B} \approx 1.56\text{GB}$$
远超常规内存限制,需改为动态节点管理。
### 优化步骤
1. **动态节点结构体**
- 每个节点仅在需要时创建
- 使用`unordered_map`存储子节点,避免空指针浪费
2. **内存释放**
- 每个测试用例结束后递归释放Trie树
3. **类型修正**
- 确保计数器使用`long long`类型
### 优化后代码
```cpp
#include <iostream>
#include <unordered_map>
#define int long long
using namespace std;
struct TrieNode {
int cnt = 0; // 实际为long long(由宏替换)
unordered_map<int, TrieNode*> children;
};
void deleteTrie(TrieNode* node) {
if (!node) return;
for (auto& pair : node->children)
deleteTrie(pair.second);
delete node;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t; cin >> t;
while (t--) {
int n, m; cin >> n >> m;
TrieNode* root = new TrieNode();
auto get = [](char c) -> int {
if (c >= 'A' && c <= 'Z') return c - 'A';
else if (c >= 'a' && c <= 'z') return c - 'a' + 26;
else return c - '0' + 52;
};
auto insert = [&](string s) {
TrieNode* p = root;
for (char ch : s) {
int x = get(ch);
if (!p->children[x])
p->children[x] = new TrieNode();
p = p->children[x];
p->cnt++; // 前缀计数+1
}
};
auto query = [&](string s) -> int {
TrieNode* p = root;
for (char ch : s) {
int x = get(ch);
if (!p->children[x]) return 0;
p = p->children[x];
}
return p->cnt;
};
// 插入所有字符串
while (n--) {
string s; cin >> s;
insert(s);
}
// 处理查询
while (m--) {
string s; cin >> s;
cout << query(s) << '\n';
}
deleteTrie(root); // 释放当前测试用例内存
}
}
```
### 关键优化点
1. **动态节点创建**
每个节点的子节点通过`unordered_map`按需创建,空间复杂度降为$O(L)$,其中$L$为所有字符串总长度。
2. **内存高效利用**
- 原方案:$3\text{e}6 \times 65 \times 8\text{B} = 1.56\text{GB}$
- 优化后:假设每个节点平均5个子节点,总内存约$3\text{e}6 \times 100\text{B} \approx 300\text{MB}$
3. **自动内存回收**
`deleteTrie`函数确保每个测试用例的内存被完全释放,避免累积内存消耗。
此方案在保证时间效率的同时,将内存消耗降低至合理范围。
阅读全文
相关推荐
















