2023/12/5 练习日志

2023/12/5 练习日志

P8630 密文搜索

题目大意

有一个字符串 \(s\)。现在有 \(n\) 个长度为 \(8\) 密码,你可以任意排列每一个密码,问你可以在 \(s\) 中匹配的有多少个。

思路

这题只是排列密码,实则匹配的时候是连续的,因此我们就可以进行统一的排序,这样子就可以达到任意排列都能匹配的效果。我们写一个哈希函数,然后将原串 \(s\) 中所有长度等于 \(8\) 的连续子字符串进行排序,然后计算哈希函数并存进数组里面。然后我们遍历 \(n\) 个密码,将这些密码都同意排序,并计算哈希值放到数组里面,如果数组里面正好有这个哈希值,那么就说明可以在 \(s\) 中匹配。

注意特殊判断 \(|s|<8\) 的情况,不然会被 Hack。

代码

#include <iostream>
#include <algorithm>
#include <string>

using namespace std;

const int kMod = 10000849;

int f[kMod], n, ans;
string s, t;

int hashFunc(string s) {     // 计算哈希值
  unsigned long long x = 0;  // 自然溢出
  for (char c : s) {         // 遍历每一个字符
    x = x * 128 + c;         // 计算哈希值
  }
  return x % kMod;           // 最后取模
}

int main() {
  cin >> s;
  if (s.size() < 8) {         // 特判 |s| < 8
    return cout << "0\n", 0;  // 直接输出 0
  }
  for (int i = 0; i < s.size() - 7; i++) { // 枚举长度为 8 的子字符串
    auto t = s.substr(i, 8);               // 获取
    sort(t.begin(), t.end());              // 排序
    f[hashFunc(t)]++;                      // 存进哈希表里面
  }
  for (cin >> n; n; n--) {     // n 个密码
    cin >> t;
    sort(t.begin(), t.end());  // 排序
    ans += f[hashFunc(t)];     // 如果有就 +1
  }
  cout << ans << '\n';         // 输出答案
  return 0;
}

P7774 KUTEVI

题目大意

\(N\) 个角,第 \(i\) 个角的大小是 \(a_i\)。接下来又有另外 \(M\) 个角,第 \(i\) 个角为 \(b_i\)。对于任意一个 \(b_i\) \((1\le i\le M)\),你需要判断是否能被若干个 \(a_i\) 之间相加减得到。(一个角可以不使用也可以使用多次)

思路

dp 题。首先,我们知道一个角的度数只属于 \([0^{\circ},359^{\circ}]\) 的范围之内的,那么我们对于角度的运算就必须要取上 \(360^{\circ}\)。我们设 \(dp_{(i,j)}\) 表示为当前已经使用了前 \(i\) 个角,能否达到 \(j^{\circ}\)。那么我们就顺理成章地得出了状态转移方程:\(dp_{(i,j)}=\max(dp_{(i,j)},dp_{(i,j+a_i)},dp_{(i,j-a_i)})\),注意取模,并且这里是布尔运算,也就是在 \(0\)\(1\) 之间取最大值。此时,我们可以看出维度 \(i\) 完全是可以省略的,那么我们就得出了优化之后的状态转移方程:

\[dp_{j\bmod 360^{\circ}}=\max(dp_{j\bmod 360^{\circ}},dp_{(j-a_i+360^{\circ})\bmod 360^{\circ}},dp_{(j+a_i)\bmod 360^{\circ}}) \]

#include <iostream>

using namespace std;

const int kMaxN = 15, kMaxD = 1005, kMod = 360;

int a[kMaxN], dp[kMaxD] = {1}, n, m;  // 记得初始化边界

int main() {
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  for (int i = 1; i <= n; i++) {                     // 已经使用过了前 i 个数
    for (int j = 0; j <= 720; j++) {                 // 尝试达到 j°,注意这里要枚举两倍
      dp[j % kMod] |= dp[(j - a[i] + kMod) % kMod];  // 从前面转移来
      dp[j % kMod] |= dp[(j + a[i]) % kMod];         // 从后面转移来
    }
  }
  for (int b; m; m--) {
    cin >> b;
    cout << (dp[b % kMod] ? "YES" : "NO") << '\n';
  }
  return 0;
}

P1379 八数码难题

题目描述

有一个 \(3\times 3\) 的棋盘,上面有 \(8\) 个棋子,有一个空位 \(0\)。现在给你了一个初始状态,每次移动你都可以把一个空位相邻的位置上面的棋子移动到空位上面。你需要将这个棋盘的状态变成 \(123804765\),问你最少需要多少次移动。保证有解。

思路

打开标签一看:可爱!竟然是 A* 算法!但是,这题的状态明明十分简单啊!棋盘一共有 \(9\) 个空位,那么状态的数量就是 \(9!=362,880‬\),非常的低。所以,大暴力广度优先搜索,启动!

我们使用一个 pair 记录状态,first 就是当前的棋盘情况,是一个 vector 套上 stringsecond 就是当前的步数。每一次搜索,我们都从棋盘中找到 \(0\) 的格子,然后向四面可以转移的方向进行转移。判重,我们就是用 set,现在你肯定知道了好渴鹅为什么要使用 STL 的原因了。然后相信自己,开上 O2,提交——AC(不得不说跑得真的慢,\(449ms\)

代码

#include <iostream>
#include <utility>
#include <vector>
#include <string>
#include <queue>
#include <set>

using namespace std;

string s;
queue<pair<vector<string>, int>> q;
set<vector<string>> f;

pair<int, int> find(const vector<string> &v) {  // 找到为 0 的位置
  for (int i = 0; i < v.size(); i++) {
    if (v[i].find('0') != string::npos) {
      return make_pair(i, v[i].find('0'));
    }
  }
}

void record(const vector<string> &v, int second) {
  if (f.count(v)) {  // 最优化剪枝
    return;
  }
  q.emplace(v, second);
  f.insert(v);
}

int bfs(const vector<string> &v) {
  for (q.emplace(v, 0); q.size(); q.pop()) {
    auto [first, second] = q.front();
    if (first == vector<string>({"123", "804", "765"})) {  // 结束了
      return second;
    }
    pair<int, int> p = find(first);
    if (p.first > 0) {  // 往上翻
      swap(first[p.first][p.second], first[p.first - 1][p.second]);
      record(first, second + 1);
      swap(first[p.first][p.second], first[p.first - 1][p.second]);
    }
    if (p.first < 2) {  // 往下翻
      swap(first[p.first][p.second], first[p.first + 1][p.second]);
      record(first, second + 1);
      swap(first[p.first][p.second], first[p.first + 1][p.second]);
    }
    if (p.second > 0) {  // 往左翻
      swap(first[p.first][p.second], first[p.first][p.second - 1]);
      record(first, second + 1);
      swap(first[p.first][p.second], first[p.first][p.second - 1]);
    }
    if (p.second < 2) {  // 向右翻
      swap(first[p.first][p.second], first[p.first][p.second + 1]);
      record(first, second + 1);
      swap(first[p.first][p.second], first[p.first][p.second + 1]);
    }
  }
  return 114514;
}

int main() {
  cin >> s;
  cout << bfs({s.substr(0, 3), s.substr(3, 3), s.substr(6, 3)}) << '\n';
  return 0;
}

P1381 单词背诵

题目大意

\(n\) 个单词需要背诵,现在有一篇 \(m\) 个单词的文章。问你在文章中你最多可以找到多少要背诵的单词,以及一段最小的连续的单词组的长度,使得文章中可以找到的最多的要背诵的单词在里面都可以找到。

思路

首先看第一问:这也不难嘛,直接一个字符串哈希完事!但是,这里需要注意一个单词出现了两次只能记录一次的值,那么我们就把出现过的改掉就行了,就假设数量为 \(c\),然后输出 \(c\)

然后就是第二问,其实也不复杂,需要用到前面的 \(c\)。对于一个序列,我们想要更新这个序列,那么我们就需要把右指针偷偷往右移,吞进更多的单词。而移动完之后,我们就需要把左边的排出去,然后进行新一轮的模拟。欸,这不就是双指针吗?我们使用 map 维护中间的这个序列,因为后可能会有重复的单词。每次我们就不断加入新元素,直到没有元素了或者不需要了,那么我们就记录答案,把左边的吐出去。

#include <iostream>
#include <unordered_map>

using namespace std;

const int kMaxN = 1e5 + 5;

int n, m, c, ans = 1e9;
string a[kMaxN], b[kMaxN];
unordered_map<string, int> f, w;

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    f[a[i]] = 1;
  }
  cin >> m;
  for (int i = 1; i <= m; i++) {
    cin >> b[i];
  }
  for (int i = 1; i <= m; i++) {
    if (f[b[i]] == 1) {
      c++, f[b[i]]++;
    }
  }
  cout << c << '\n';
  for (int i = 1, j = 1; i <= m; i++) {
    for (; j <= m && w.size() < c; j++) {  // 获取新单词
      if (f[b[j]]) {                       // 必须是想要背的
        w[b[j]]++;                         // 加进来
      }
    }
    if (w.size() == c) {                   // 满足要求
      ans = min(ans, max(0, j - i));       // 取 min 值
    }
    if (f[b[i]]) {      // 如果头部单词是想要背的
      w[b[i]]--;        // 删掉一个
      if (!w[b[i]]) {   // 如果数量为 0
        w.erase(b[i]);  // 全部删掉
      }
    }
  }
  cout << ans << '\n';
  return 0;
}

P7469 积木小赛

题目大意

两个人有 \(n\) 块积木,第一个人可以选择保留任意不为空的积木,第二个人可以丢掉两边的积木,问你有多少种他们两个的积木一模一样的可能。

思路

首先我们知道,第二个人的限制条件要更紧迫,那我们就枚举第二个人丢掉的左端点 \(i\),然后线性往右扫,每遇到一个新元素就去匹配第一个人的积木。因为第二个人的右端点是不确定的,因此每次都记录答案。这是,哈希的作用就体现出来了,我们就可以把序列哈希的值存到一个数组里面,然后排序、去重,最后数组的长度就是答案的数量了。

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;
using ll = unsigned long long;

const ll kMaxN = 3005, kMod = 212370440130137957ll;

ll n;
char a[kMaxN], b[kMaxN];
vector<ll> v;

int main() {
  cin >> n;
  for (ll i = 1; i <= n; i++) {
    cin >> a[i];
  }
  for (ll i = 1; i <= n; i++) {
    cin >> b[i];
  }
  for (ll i = 1; i <= n; i++) {                          // 第二个人的第一个积木
    ll x = 0;                                            // 哈希值
    for (ll j = i, p = 1; j <= n && p <= n; j++, p++) {  // j 表示第二份人的积木,p 表示第一个人
      for (; p <= n && a[p] != b[j]; p++) { }            // 找到下一个匹配第二个人的
      if (p <= n) {                                      // 如果匹配到了
        x = (x * 114 + b[j]) % kMod;                     // 计算哈希值
        v.push_back(x);                                  // 记录哈希值
      }
    }
  }
  sort(v.begin(), v.end());                      // 排序
  v.erase(unique(v.begin(), v.end()), v.end());  // 去重并删除
  cout << v.size() << '\n';                      // 输出答案
  return 0;
}
posted @ 2023-12-05 18:00  haokee  阅读(8)  评论(0)    收藏  举报