【题单】字典树基础

数据结构题单 208. 实现 Trie (前缀树) 总共有 $3 \cdot 10^4$ 次插入,每个单词长度 $2000$,字母个数最多是 $6 \cdot 10^7$ 个,考虑到前缀重复的情况,估算字典树中的节点是 $3 \cdot 10^5$ 个 const int N = 300100; struct TrieTree { int n, idx; vector<VI> son; VI cnt; TrieTree() {} TrieTree(int _n) : n(_n), idx(0) { son = vector<VI>(n, VI(26, -1)); cnt = VI(n, 0); } void insert(string &s) { int p = 0; for (auto c : s) { int u = c - 'a'; if (son[p][u] == -1) son[p][u] = ++idx; p = son[p][u]; } cnt[p]++; } bool search(string &s) { int p = 0; for (auto c : s) { int u = c - 'a'; if (son[p][u] == -1) return false; p = son[p][u]; } return cnt[p] > 0; } bool starts_with(string &s) { int p = 0; for (auto c : s) { int u = c - 'a'; if (son[p][u] == -1) return false; p = son[p][u]; } return true; } }; class Trie { public: TrieTree tr; Trie() { tr = TrieTree(N); } void insert(string word) { tr.insert(word); } bool search(string word) { return tr.search(word); } bool startsWith(string prefix) { return tr.starts_with(prefix); } }; 更好的做法是使用动态创建节点的方式,避免估算不准确。 ...

October 21, 2024 · 10 min · 2005 words

【题单】对顶堆

数据结构题单 2102. 序列顺序查询 维护一个小顶堆 q1,代表前 k 大的元素。 维护一个大顶堆 q2,代表其它元素。 当新加一个元素的时候,首先加入 q2,当 q1 中不足 k 个元素的时候,把 q2 的最大元素移动到 q1。如果 q1 的堆顶小于 q2 的堆顶,此时应该交换堆顶,这个交换操作在一次函数调用中最多会发生一次,因为新加入元素的时候,只会产生一个元素的变化。 另外需要注意自定义对象的比较函数的时候,因为是求第 k 大,并且字典序最小,应该让分数正序排序,名字逆序排序,这样当分数相同的时候,靠后的是字典序更小的。 class SORTracker { public: struct node { int val; string name; bool operator<(const node &rh) const { if (val != rh.val) return val < rh.val; return name > rh.name; } bool operator>(const node &rh) const { if (val != rh.val) return val > rh.val; return name < rh.name; } }; pqg<node> q1; pq<node> q2; int cur; SORTracker() : q1{}, q2{}, cur{1} {} void add(string name, int score) { node tmp = {score, name}; q2.push(tmp); while (SZ(q1) < cur && nemp(q2)) { q1.push(q2.top()); q2.pop(); } while (nemp(q1) && nemp(q2) && q1.top() < q2.top()) { auto t1 = q1.top(); q1.pop(); auto t2 = q2.top(); q2.pop(); q1.push(t2); q2.push(t1); } } string get() { while (SZ(q1) < cur && nemp(q2)) { auto t = q2.top(); q1.push(t); q2.pop(); } cur++; return q1.top().name; } }; 题解区的神奇思路。 ...

October 19, 2024 · 4 min · 688 words

【题单】位运算——拆位法

位运算题单 477. 汉明距离总和 先考虑两个数字的情况,按位考虑,如果二进制位不同,那么对答案的贡献是 1. 接下来考虑多个数字的情况,同样按位考虑,对于某一位来说,设 0 的个数是 $c_1$,1 的个数是 $c_2$,那么这一位对答案的贡献是 $c_1 \cdot c_2$ int totalHammingDistance(vector<int> &a) { int ans{}; For1(i, 0, 30) { int c1{}, c2{}; for (auto x : a) { if ((x >> i) & 1) c1++; else c2++; } ans += c1 * c2; } return ans; } 1863. 找出所有子集的异或总和再求和 因为数组长度只有 12,考虑暴力,枚举所有的子集,暴力计算每个子集的异或和。 int subsetXORSum(vector<int> &a) { int n{SZ(a)}, ans{}; For(i, 0, (1 << n)) { int cur{}; For(j, 0, n) { if ((i >> j) & 1) { cur ^= a[j]; } } ans += cur; } return ans; } 题解区的神奇做法。 按位考虑,先考虑第 $i$ 位,如果一个集合中,第 $i$ 位是 1 的数字的个数是奇数,那么这个集合在第 $i$ 位对答案有贡献,并且贡献等于 $2^i$。 接下来考虑有多少个集合在第 $i$ 位对答案有贡献。也就是求有多少个集合,满足集合内的所有数字在第 $i$ 位是 1 的个数是奇数。设数组内所有数字在第 $i$ 位是 1 的个数是 $m$,那么包含至少一个 1 的集合的个数是 $2^m - 1$ 个。其中包含奇数个 1 的集合的个数是 $\sum{2^{n - m} {m \choose k}}$ 其中 $k$ 是奇数。根据二项式公式,能够得到当 $k$ 是奇数的二项式系数之和是 $2^{m - 1}$,因此上面的和是 $2^{n - 1}$. ...

October 17, 2024 · 3 min · 449 words

【题单】不定长滑动窗口——求最长或最大

滑动窗口题单 3. 无重复字符的最长子串 维护一个不定长的滑动窗口: 加入右端点 当不满足条件的时候不断移动左端点 维护答案 int lengthOfLongestSubstring(string s) { int ans{}, n = SZ(s); map<char, int> m; for (int i = 0, j = 0; i < n; ++i) { m[s[i]]++; while (m[s[i]] > 1 && j < i) m[s[j++]]--; ckmax(ans, i - j + 1); } return ans; } 题解区的另外一种写法。 当右端点在窗口中已经存在的时候,不断移动左指针,直到窗口中不存在右端点。 插入右端点 维护答案 int lengthOfLongestSubstring(string s) { int ans{}, n = SZ(s); set<char> vis; for (int i = 0, j = 0; i < n; ++i) { while (has(vis, s[i])) { vis.erase(s[j++]); } vis.insert(s[i]); ckmax(ans, i - j + 1); } return ans; } 3090. 每个字符最多出现两次的最长子字符串 当窗口中的右端点对应的字符出现了至少两次的时候,移动左指针,直到它出现小于两次 加入右端点 维护答案 int maximumLengthSubstring(string s) { VI a(26); int n{SZ(s)}, ans{}; for (int i = 0, j = 0; i < n; ++i) { while (a[s[i] - 'a'] >= 2) { a[s[j++] - 'a']--; } a[s[i] - 'a']++; ckmax(ans, i - j + 1); } return ans; } 1493. 删掉一个元素以后全为 1 的最长子数组 维护一个滑动窗口,满足窗口内部的 0 的个数最多有 1 个,求窗口内部去掉 1 个数字之后的连续的 1 的个数。 ...

October 17, 2024 · 7 min · 1482 words

【题单】二叉树自顶向下 DFS

二叉树题单 104. 二叉树的最大深度 深度从根开始往下递增,到达叶子的时候,深度达到最深,更新答案。 int ans; void dfs(TNP root, int d) { if (!root) return; auto [_, l, r] = *root; if (!l && !r) ckmax(ans, d); dfs(l, d + 1); dfs(r, d + 1); } int maxDepth(TreeNode *root) { ans = 0; dfs(root, 1); return ans; } 111. 二叉树的最小深度 从根往下访问,访问到孩子节点的时候,深度加一。到达叶子节点的时候更新答案。 int ans; void dfs(TNP root, int d) { if (!root) return; auto [_, l, r] = *root; if (!l && !r) ckmin(ans, d); dfs(l, d + 1); dfs(r, d + 1); } int minDepth(TreeNode *root) { ans = INF; dfs(root, 1); if (ans == INF) ans = 0; return ans; } 112. 路径总和 从根开始累加当前路径的和,如果到叶子的和与目标值相等,那么找到答案。 如果不是叶子,那么分别递归的左右子树。 int target; bool ans; void dfs(TNP root, int cur) { if (ans) return; if (!root) return; int val = root->val; auto left = root->left, right = root->right; if (!left && !right) { if (cur + val == target) { ans = true; return; } } else if (!left) { dfs(right, cur + val); } else if (!right) { dfs(left, cur + val); } else { dfs(left, cur + val); dfs(right, cur + val); } } bool hasPathSum(TreeNode *root, int targetSum) { target = targetSum; ans = false; dfs(root, 0); return ans; } 另外一种更好的思路:从根节点到叶子节点的过程中,不断从 targetSum 中减去当前节点的值,如果到达叶子的时候 targetSum 恰好等于叶子节点的值,那么找到了答案。否则递归左子树或者右子树。 ...

October 17, 2024 · 10 min · 1998 words

【题单】反转链表

链表题单 206. 反转链表 p1 指向前一个节点,p2 指向当前节点,p3 指向下一个节点。 每次只修改 p2 节点,使它指向 p1,然后向右移动 p1, p2,p3 的作用是记录下一个 p2 的位置。 当 p2 为空时,说明整个链表反转结束。记得把原链表头的 next 指针置为空。 ListNode *reverseList(ListNode *head) { if (!head) return head; LNP p1 = head; LNP p2 = p1->next; LNP p3{NULL}; while (p2) { p3 = p2->next; p2->next = p1; p1 = p2; p2 = p3; } head->next = NULL; return p1; } 92. 反转链表 II 这道题目使用一次遍历看起来复杂,其实只要观察到边界条件,其实是不难,多使用几个变量保存需要保存的信息更清晰。 首先我们需要找到要反转区间的前一个位置 st,然后找到反转区间的起点和终点 q1, q2,然后找到反转区间的下一个节点 en。找到 st 需要从 ro 节点走 left - 1 步,找到 q2 节点需要从 ro 节点走 r 步。这两步可以使用一次遍历得到。接下来就是常规的反转链表。 ListNode *reverseBetween(ListNode *head, int left, int right) { LNP ro{new LN(0, head)}; LNP p1{ro}; int cnt{}; while (cnt < left - 1) { cnt++; p1 = p1->next; } LNP st = p1; LNP q1{st->next}; LNP p2{p1->next}; LNP p3{}; while (cnt < right) { p3 = p2->next; p2->next = p1; p1 = p2; p2 = p3; cnt++; } LNP q2{p1}; LNP en{p2}; st->next = q2; q1->next = en; LNP res = ro->next; delete ro; return res; } 24. 两两交换链表中的节点 这道题和反转一个区间内的链表类似,只不过区间长度是 2。 ...

October 17, 2024 · 3 min · 590 words

LeetCode Contest 390 D 最长公共后缀查询

最长公共后缀查询 要求最长的公共后缀字符串,可以把每个字符串逆序,存到字典树里面,在字典树的节点上保存到达当前节点并且原字符串长度最短的字符串长度和下标。对于给定字符串,直接从字典树中查找当前字符串,把树上能够找到的最后一个节点上的信息返回即可。 初始化的时候,先找到字典树使用到的节点的最大值,只初始化使用到的节点,如果 memset 整个数组会超时。 // Date: Sun Mar 24 22:48:03 2024 #include <climits> #include <cmath> #include <cstdio> #include <cstdlib> #include <cstring> #include <algorithm> #include <functional> #include <iomanip> #include <iostream> #include <map> #include <queue> #include <set> #include <sstream> #include <stack> #include <string> #include <utility> #include <vector> using namespace std; typedef long long ll; typedef unsigned long long ull; typedef vector<int> VI; typedef pair<int, int> PII; typedef pair<ll, ll> PLL; template <class T> using pq = priority_queue<T>; template <class T> using pqg = priority_queue<T, vector<T>, greater<T>>; const int INF = 0x3f3f3f3f, MOD = 1e9 + 7, MOD1 = 998'244'353; const ll INFL = 0x3f3f3f3f'3f3f3f3f; const double eps = 1e-8; const int dir[8][2] = { {0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}, }; const ull Pr = 131; #define For(i, a, b) for (int i = int(a); i < int(b); ++i) #define Rof(i, a, b) for (int i = int(b) - 1; i >= int(a); --i) #define For1(i, a, b) for (int i = int(a); i <= int(b); ++i) #define Rof1(i, a, b) for (int i = int(b); i >= int(a); --i) #define ForE(i, j) for (int i = h[j]; i != -1; i = ne[i]) #define f1 first #define f2 second #define pb push_back #define has(a, x) (a.find(x) != a.end()) #define nemp(a) (!a.empty()) #define all(a) (a).begin(), (a).end() #define SZ(a) int((a).size()) #define NL cout << '\n'; template <class T> bool ckmin(T &a, const T &b) { return b < a ? a = b, 1 : 0; } template <class T> bool ckmax(T &a, const T &b) { return a < b ? a = b, 1 : 0; } template <typename t> istream &operator>>(istream &in, vector<t> &vec) { for (t &x : vec) in >> x; return in; } template <typename t> ostream &operator<<(ostream &out, vector<t> &vec) { int n = SZ(vec); For(i, 0, n) { out << vec[i]; if (i < n - 1) out << ' '; } return out; } void __print(int x) { cerr << x; } void __print(long x) { cerr << x; } void __print(long long x) { cerr << x; } void __print(unsigned x) { cerr << x; } void __print(unsigned long x) { cerr << x; } void __print(unsigned long long x) { cerr << x; } void __print(float x) { cerr << x; } void __print(double x) { cerr << x; } void __print(long double x) { cerr << x; } void __print(char x) { cerr << '\'' << x << '\''; } void __print(const char *x) { cerr << '\"' << x << '\"'; } void __print(const string &x) { cerr << '\"' << x << '\"'; } void __print(bool x) { cerr << (x ? "true" : "false"); } template <typename T, typename V> void __print(const pair<T, V> &x) { cerr << '{'; __print(x.first); cerr << ", "; __print(x.second); cerr << '}'; } template <typename T> void __print(const T &x) { int f = 0; cerr << '{'; for (auto &i : x) cerr << (f++ ? ", " : ""), __print(i); cerr << "}"; } void _print() { cerr << "]\n"; } template <typename T, typename... V> void _print(T t, V... v) { __print(t); if (sizeof...(v)) cerr << ", "; _print(v...); } #ifdef _DEBUG #define debug1(x) cout << #x " = " << x << endl; #define debug2(x, y) cout << #x " = " << x << " " #y " = " << y << endl; #define debug3(x, y, z) \ cout << #x " = " << x << " " #y " = " << y << " " #z " = " << z << endl; #define dbg(x...) \ cerr << "\e[91m" << __func__ << ":" << __LINE__ << " [" << #x << "] = ["; \ _print(x); \ cerr << "\e[39m" << endl; #else #define debug1 #define debug2 #define debug3 #define dbg(x...) #endif // For LeetCode #define LN ListNode #define LNP ListNode * #define TN TreeNode #define TNP TreeNode * #ifdef _DEBUG struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int val) : val(val), next(nullptr) {} ListNode(int val, ListNode *next) : val(val), next(next) {} }; struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode() : val(0), left(nullptr), right(nullptr) {} TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} }; #endif // End of LeetCode const int N = 500100; int idx, son[N][26]; struct po { int len, i; }; po val[N]; void Insert(string s, int i) { int p = 0, cur = SZ(s); auto &t = val[p]; if (!t.len || ckmin(t.len, cur)) { t = po{cur, i}; } for (auto c : s) { int u = c - 'a'; if (!son[p][u]) son[p][u] = ++idx; p = son[p][u]; auto &t = val[p]; if (!t.len || ckmin(t.len, cur)) { t = po{cur, i}; } } } int Find(string s) { int p = 0; for (auto c : s) { int u = c - 'a'; if (!son[p][u]) { break; } p = son[p][u]; } return val[p].i; } class Solution { public: vector<int> stringIndices(vector<string> &a, vector<string> &b) { int sum{}; for (auto s : a) sum += SZ(s); idx = 0; For1(i, 0, sum) { For(j, 0, 26) son[i][j] = 0; val[i] = po{0, 0}; } int n = SZ(a); VI res; For(i, 0, n) { string s = a[i]; reverse(all(s)); Insert(s, i); } for (auto s : b) { reverse(all(s)); res.pb(Find(s)); } return res; } }; #ifdef _DEBUG int main(void) { std::ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); Solution a; return 0; } #endif

March 24, 2024 · 5 min · 932 words