一.字符串哈希
字符串哈希概述
字符串哈希是一种将字符串映射到一个数值的技术,常用于处理字符串相关的算法问题,尤其在处理字符串匹配、子串查找等问题时非常高效。它的核心思想是利用一个哈希函数将字符串映射成一个整数,并根据该整数来判断字符串是否相等。
字符串哈希的结构
字符串哈希的结构包括:
- 哈希值(Hash Value):通过某种算法计算得到的一个整数值,代表该字符串的“身份”。
- 哈希函数:将字符串映射到哈希值的公式或算法。常见的哈希函数基于多项式哈希或者改进后的滚动哈希。
- 哈希表:一种存储字符串哈希值的容器,通常使用数组或字典。
哈希函数:多项式哈希
一个常见的字符串哈希函数是基于多项式的哈希:
给定一个字符串 s = s_0 s_1 s_2 ... s_n-1
,其哈希值可以通过如下公式计算:
p
是一个常数基数,通常选择一个小的质数(例如 31 或 53)。m
是模数,用于避免哈希值过大,通常选择一个大质数。s_i
是字符串中的每个字符转换为整数后的值,通常是字符的 ASCII 码或字母位置。
字符串哈希的功能
- 高效比较:通过哈希值,两个字符串的相等性可以通过哈希值的比较来实现,避免逐字符对比,提高效率。
- 子串查找:通过哈希技术,快速计算任意子串的哈希值,可以用于解决许多字符串匹配的问题。
- 避免重复:哈希值可以帮助快速去重,例如在长字符串中查找重复的子串。
-
在线算法:滚动哈希支持在线更新,即当一个子串被扩展或者滑动时,可以通过常数时间更新哈希值。
代码实现
// 俩字符串是否相等
#include <iostream>
#include <string>
#include <cmath>const int p = 31;
const int m = 1e9 + 7;long long compute_hash(const std::string &s) {long long hash_value = 0;long long p_pow = 1;for (char c : s) {hash_value = (hash_value + (c - 'a' + 1) * p_pow) % m;p_pow = (p_pow * p) % m;}return hash_value;
}int main() {std::string s1 = "hello";std::string s2 = "hello";if (compute_hash(s1) == compute_hash(s2)) {std::cout << "Strings are equal." << std::endl;} else {std::cout << "Strings are not equal." << std::endl;}return 0;
}
/*2. 滚动哈希(子串比较)
通过滚动哈希,我们可以高效地比较字符串中的任意子串。在这个过程中,哈希值会根据前一个哈希值和新加入的字符更新。
*/
#include <iostream>
#include <string>
#include <vector>const int p = 31;
const int m = 1e9 + 7;std::vector<long long> compute_prefix_hash(const std::string &s) {int n = s.size();std::vector<long long> prefix_hash(n + 1, 0);long long p_pow = 1;for (int i = 0; i < n; ++i) {prefix_hash[i + 1] = (prefix_hash[i] + (s[i] - 'a' + 1) * p_pow) % m;p_pow = (p_pow * p) % m;}return prefix_hash;
}long long get_substring_hash(int l, int r, const std::vector<long long> &prefix_hash) {long long hash_value = prefix_hash[r + 1] - prefix_hash[l];if (hash_value < 0) hash_value += m;return hash_value;
}int main() {std::string s = "abcdef";auto prefix_hash = compute_prefix_hash(s);// 比较 s[1..3] 和 s[2..4] 是否相同if (get_substring_hash(1, 3, prefix_hash) == get_substring_hash(2, 4, prefix_hash)) {std::cout << "Substrings are equal." << std::endl;} else {std::cout << "Substrings are not equal." << std::endl;}return 0;
}
二.题
1.
思路:A - B = C可以变为 A - C = B,通过统计每个数字出现的次数,然后对其本身a[i] -= C,对映映射到map里的位置就是B
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
#include <cstring>
#include <map>using namespace std;
using LL = long long;LL a[200001];
map<LL, LL> m; int main() {int n;LL c;LL ans = 0;cin >> n >> c;for (int i = 1; i <= n; i++) {cin >> a[i];m[a[i]]++;a[i] -= c;}for (int i = 1; i <= n; i++) ans += m[a[i]];cout << ans << endl;return 0;
}
2.
思路:根据题意,字符串哈希统计每个位置的前缀哈希值,通过for对字符串a和字符串b的前缀和后缀的哈希值进行比对,求最大值
#include <iostream>
#include <vector>
#include <stack>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <climits>
#include <map>using namespace std;const int N = 110, base = 131;
long long p[N], hl[N], hr[N];
char stra[N], strb[N];
int lena, lenb, lmax;long long get(long long h[], int l, int r){return h[r] - h[l - 1] * p[r - l + 1];
}
int main(){int i, j, res = INT_MIN;scanf("%s%s", stra + 1, strb + 1);lena = strlen(stra + 1);lenb = strlen(strb + 1);lmax = max(lena, lenb); p[0] = 1;for (i = 1; i <= lena; i++) hl[i] = hl[i - 1] * base + (stra[i] - 96);for (i = 1; i <= lenb; i++) hr[i] = hr[i - 1] * base + (strb[i] - 96);for (i = 1; i <= lmax; i++) p[i] = p[i - 1] * base;for (i = 1; i <= lmax; i++){int al, bl;if (lena < i || lenb < i) break; al = get(hl, 1, i) != get(hr, lenb - i + 1, lenb) ? INT_MIN : i;bl = get(hl, lena - i + 1, lena) != get(hr, 1, i) ? INT_MIN : i; res = max(res, max(al, bl)); }printf("%d\n", res);return 0;
}
3.
思路 :用集合来储存每个不同字符串的哈希值,最后求集合的长度即可
#include <iostream>
#include <vector>
#include <stack>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <climits>
#include <map>
#include <set>using namespace std;const int m = 131;
const int p = 1e9 + 7;int prefix_hash(string& s) {int p_pow = 1;int prefix = 0;for (int i = 0; i < s.size(); i++) {prefix = (prefix + (s[i] - 'a' - 1) * p_pow) % p;p_pow = (p_pow * m) % p;}return prefix;
}int main(){int n; cin >> n;vector<string>s(n+1);set<int>sett;for (int i = 1; i <= n; i++) {cin >> s[i];int temp = prefix_hash(s[i]);sett.insert(temp);}int coun = sett.size();cout << coun;return 0;
}
4.
思路: 用哈希表存集合,每个单词都有对于的文章,通过输入的单词找文章即可
#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <string>using namespace std;int main() {int n;cin >> n;map<string, set<int>> word_to_passages;for (int i = 1; i <= n; i++) {int words;cin >> words;for (int j = 0; j < words; j++) {string word;cin >> word;word_to_passages[word].insert(i);}}int m;cin >> m;while (m--) {string word;cin >> word;if (word_to_passages.find(word) != word_to_passages.end()) {for (int passage : word_to_passages[word]) {cout << passage << " ";}}cout << endl;}return 0;
}
5.
思路:哈希集合+容器
#include <iostream>
#include <vector>
#include <stack>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include <set>using namespace std;int main(){int t; cin >> t;while (t--) {int n; cin >> n;vector<int>tt;unordered_set<int>ttt;for (int i = 1; i <= n; i++) {int temp; cin >> temp;if (ttt.find(temp) == ttt.end()) {tt.push_back(temp);ttt.insert(temp);}}for (int i = 0; i < tt.size(); i++) {cout << tt[i] << " ";}cout << endl;}return 0;
}
6.算法竞赛进阶指南
思路:用埃拉托斯特尼筛法
#include <iostream>
#include <vector>
using namespace std;const int MAX_N = 100000000; void sieve_of_eratosthenes(int n, vector<int>& primes) {vector<bool> is_prime(n + 1, true);is_prime[0] = is_prime[1] = false; for (int i = 2; i * i <= n; ++i) {if (is_prime[i]) {for (int j = i * i; j <= n; j += i) {is_prime[j] = false;}}}for (int i = 2; i <= n; ++i) {if (is_prime[i]) {primes.push_back(i);}}
}int main() {ios::sync_with_stdio(0); int n, q;cin >> n >> q;vector<int> primes;sieve_of_eratosthenes(n, primes);while (q--) {int k;cin >> k;cout << primes[k - 1] << endl; }return 0;
}
三.动态规划
状态机 DP(Dynamic Programming, 状态机动态规划)
状态机DP是一种将动态规划(DP)与有限状态机(FSM, Finite State Machine)相结合的策略。它通常用于解决一些有多个状态转换的问题,每个状态依赖于前一个状态或多个前状态的结果。
核心思想
状态机 DP 是将问题的状态划分成若干个不同的子问题,使用 DP 来保存每个状态的结果,并在状态之间进行转移。每次状态转移时会考虑当前状态和之前状态的关系,通常通过某些条件进行转换。
在很多实际问题中,可以通过建模成状态机,明确每个状态的含义,并根据问题的具体要求来设计状态转移的规则和相应的 DP 转移方程。
一般步骤
-
定义状态:明确在问题中你需要维护哪些状态,可以是某个数值的集合,也可以是某些特定的条件或标志。
-
状态转移:定义如何从当前状态转移到下一个状态。转移条件通常由问题本身的限制或约束决定。
-
递推关系:写出状态转移方程,描述从前一个状态如何得到当前状态的值。
-
初始化:定义状态的初始值,通常是边界条件。
-
答案:根据最终的状态或者状态值来得到问题的答案。
状态机 DP 的经典应用
- 多重背包问题:通过状态来记录背包的容量和物品的状态,进行状态转移。
- 字符串匹配:例如求解正则表达式的匹配问题,或者KMP算法的状态机。
- 最短路径问题:特别是在有多个条件和约束的情况下,状态机可以很好地建模状态和转移。
- 路径规划问题:比如在网格中进行路径搜索,并在状态机中管理每个路径的不同状态。
例子1:0-1背包问题的状态机 DP
我们可以将 0-1 背包问题中的状态定义为背包的容量。状态机 DP 的状态就是当前已放入背包的物品集合的状态。
问题:有一个背包容量为 W
,现在有 N
个物品,第 i
个物品的重量为 wi
,价值为 vi
。要求在不超过背包容量的情况下,使得背包中的物品总价值最大。
状态定义:
dp[i][w]
:表示前i
个物品,背包容量为w
时的最大价值。
状态转移:
- 不选择第
i
个物品:dp[i][w] = dp[i-1][w]
- 选择第
i
个物品:dp[i][w] = dp[i-1][w - wi] + vi
(前提是w >= wi
)
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main() {int n, W;cin >> n >> W;vector<int> w(n), v(n);// 输入物品的重量和价值for (int i = 0; i < n; i++) {cin >> w[i] >> v[i];}// dp[w]表示背包容量为w时的最大价值vector<int> dp(W + 1, 0);for (int i = 0; i < n; i++) {// 从后往前遍历避免重复计算for (int j = W; j >= w[i]; j--) {dp[j] = max(dp[j], dp[j - w[i]] + v[i]);}}cout << dp[W] << endl; // 输出最大价值return 0;
}
解释:
dp[j]
表示容量为j
时的最大价值。- 对于每个物品
i
,我们从后向前遍历容量W
,更新背包的最大价值。
例子2:字符串匹配(正则表达式)中的状态机 DP
在正则表达式匹配问题中,可以通过状态机来表示正则表达式的每个状态。
问题:给定一个字符串 s
和一个模式 p
,判断 s
是否与模式 p
匹配,模式中的 .
可以匹配任意字符,*
可以匹配零个或多个前面的字符。
状态定义:
dp[i][j]
:表示s[0..i-1]
是否与p[0..j-1]
匹配。
状态转移:
- 如果
p[j-1]
是*
:dp[i][j] = dp[i][j-2]
或者dp[i-1][j]
(即*
匹配零次或多次)
- 如果
p[j-1]
是.
或者s[i-1] == p[j-1]
:dp[i][j] = dp[i-1][j-1]
#include <iostream>
#include <vector>
#include <string>using namespace std;bool isMatch(const string& s, const string& p) {int m = s.size(), n = p.size();vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));dp[0][0] = true; // 空串和空模式是匹配的// 处理模式中以 * 开头的情况for (int j = 1; j <= n; j++) {if (p[j - 1] == '*') {dp[0][j] = dp[0][j - 2];}}for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (p[j - 1] == s[i - 1] || p[j - 1] == '.') {dp[i][j] = dp[i - 1][j - 1];} else if (p[j - 1] == '*') {dp[i][j] = dp[i][j - 2] || (dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));}}}return dp[m][n];
}int main() {string s = "aa";string p = "a*";if (isMatch(s, p)) {cout << "Matched!" << endl;} else {cout << "Not matched!" << endl;}return 0;
}
结论
- 状态机 DP 结合了动态规划的思想和状态机的建模,通过定义状态、转移规则和递推关系,解决了很多具有状态转移性质的问题。
- 典型的应用包括多重背包问题、字符串匹配问题、最短路径问题等。
- 在使用状态机 DP 时,要重点理解状态的定义,状态之间的转移条件,以及如何通过 DP 更新每个状态的值。
四.题
1.
思路:由题来模拟整个天数的交易过程,每次都做出最优解
class Solution {
public:int maxProfit(int k, vector<int>& prices) {int n = prices.size();vector f(n + 1, vector<array<int, 2>>(k + 2, {INT_MIN / 2, INT_MIN / 2}));for (int j = 1; j <= k + 1; j++) {f[0][j][0] = 0;}for (int i = 0; i < n; i++) {for (int j = 1; j <= k + 1; j++) {f[i + 1][j][0] = max(f[i][j][0], f[i][j][1] + prices[i]);f[i + 1][j][1] = max(f[i][j][1], f[i][j - 1][0] - prices[i]);}}return f[n][k + 1][0];}
};