2022杭电多校第一场补题
显然还没补完
题目
A. String
KMP、Border树
题意
找字符串 \(S\) 的所有前缀子串 \(S_i\),所有 \(border\) 长度 \(b\) 满足 \((2*b - i) \% k =0\) 的 \(border\) 数量。
数据范围
\(|S|\leq 10^6\)
解法一, 建 border 树硬搞 \(O(nlogn)\)
思路
- 将题目要求经过简单的化简就是题意,先建立 border 树。
- 对于 border 树中每个节点寻找满足要求的祖先节点的个数。
- 考虑 dfs 同时开 \(k\) 个 vector 即
vector<int> cnt[K]
来记录 \(\%k=i\) 的数字。- 每遍历一个点进行
cnt[2 * u % k].push_back(2 * u)
。 - 然后因为 dfs 过程中通过回溯记录的是根到节点一条链上的信息,所以利用二分查询
cnt[u % k]
中大于 \(u\) 的个数记录为ans[i]
为节点最终的答案。
- 每遍历一个点进行
- 最后统计答案即可,然后因为SB杭电,恭喜我们 \(1e6\) 的递归层数也爆栈了,所以需要手搓一个递归栈。
- 时间复杂度 \(O(nlogn)\),应该有别的解法,待补。
Solution
#include<bits/stdc++.h>
#define fi first
#define pb push_back
#define se second
#define arr(x) x.begin(),x.end()
#define endl '\n'
using namespace std;
typedef std::pair<int, int> PII;
typedef long long ll;
const int mod = 998244353, N = 1e6 + 10;
char s[N];
vector<int> edge[N], cnt[N];
int ne[N], k;
ll ans[N];
void get_ne(const char* s, int len) {
ne[1] = 0;
edge[0].pb(1);
for (int i = 2, j = 0; i <= len; i++) {
while (j && s[j + 1] != s[i]) j = ne[j];
if (s[i] == s[j + 1]) j ++;
ne[i] = j;
edge[j].pb(i);
}
return ;
}
// 实际dfs
void dfs(int u) {
int val = 2 * u % k;
cnt[val].pb(2 * u);
int r = u % k;
if(cnt[r].size())
ans[u] = cnt[r].size() - (upper_bound(arr(cnt[r]), u) - cnt[r].begin());
else ans[u] = 0;
for (auto v: edge[u]) {
dfs (v);
}
cnt[val].pop_back();
}
// 手写栈,里面变量不能放多了,不然也炸 :(
struct Data {
int u, idx;
};
stack<Data> stk;
int top = -1;
void solve(int x) {
stk.push((Data){x, 0});
while (!stk.empty()) {
auto now = stk.top();
stk.pop();
int u = now.u, idx = now.idx, sz = edge[u].size();
int val = 2 * u % k, r = u % k;
if (!idx) {
cnt[val].pb(2 * u);
if(cnt[r].size())
ans[u] = cnt[r].size() - (upper_bound(arr(cnt[r]), u) - cnt[r].begin());
else ans[u] = 0;
}
if (idx < sz) {
stk.push((Data){u, idx+1});
stk.push((Data){edge[u][idx], 0});
}
else {
cnt[val].pop_back();
}
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
cin >> (s + 1);
int n = strlen(s + 1);
for (int i = 0; i <= n; i++) edge[i].clear();
get_ne(s, n);
cin >> k;
// dfs(0);
solve(0);
ll res = 1;
for (int i = 1; i <= n; i++) {
res = res * (ans[i] + 1) % mod;
ans[i] = 0;
}
cout << res << endl;
}
return 0;
}
B. Dragon slayer
二进制枚举、dfs/bfs、坐标放大
题意
在 \(n\times m\) 的方格图上,有 \(k\) 个横或竖的墙,起始坐标 \((x_s+0.5,y_s+0.5)\),结尾坐标 \((x_t+0.5,y_t+0.5\) 。问至少破坏多少面墙才能从起点到终点。
数据范围
\(1\leq n,m,k\leq 15\)
\(0\leq x_s,x_t\leq n, 0\leq y_s,y_t\leq m\)
每面墙坐标: \(0\leq x_1,x_2\leq n, 0\leq y_1,y_2\leq m, x_1=x_2\; or\; y_1=y_2\)
思路
- \(k\) 范围很小,先枚举剩下了那些墙不能撞, \(O(2^k)\)
- 因为起始坐标很奇怪,不妨将坐标全部放大两倍,每次移动距离为 \(2\)。
- 然后进行 \(dfs/bfs\) 判断能否达到终点,每次移动判断起点到对应终点的中点是否是不能被破坏的墙,如果不是就讲继续搜下一个点。
- 然后就做完了,时间复杂度 \(O(2^k*k*n*m)\)
Solution
#include<bits/stdc++.h>
#define fi first
#define se second
#define arr(x) x.begin(),x.end()
#define endl '\n'
using namespace std;
typedef std::pair<int, int> PII;
typedef long long ll;
const int mod = 1e9 + 7;
#define y1 sajdakwjdiasjidawij
int _read() {
static int ans, c, p;
for (c = getchar(); c != '-' && (c < '0' || c > '9'); c = getchar());
if (c == '-') p = false, c = getchar(); else p = true;
for (ans = 0; c <= '9' && c >= '0'; c = getchar()) ans = ans * 10 + c - '0';
return p ? ans : -ans;
}
void _write(int ans) {
static int a[20], n;
if (ans < 0) {
putchar('-');
ans = -ans;
}
if (ans == 0) {
putchar('0');
return;
}
for (n = 0; ans; ans /= 10) a[n++] = ans % 10;
for (n--; n >= 0; n--) putchar(a[n] + '0');
return;
}
int n, m, k, xs, ys, xt, yt, wall;
int x1[16], x2[16], y1[16], y2[16];
void init() {
n = _read() * 2;
m = _read() * 2;
k = _read();
xs = _read() * 2 + 1;
ys = _read() * 2 + 1;
xt = _read() * 2 + 1;
yt = _read() * 2 + 1;
for (int i = 0; i < k; i++) {
x1[i] = _read() * 2;
y1[i] = _read() * 2;
x2[i] = _read() * 2;
y2[i] = _read() * 2;
if (x1[i] > x2[i]) swap(x1[i], x2[i]);
if (y1[i] > y2[i]) swap(y1[i], y2[i]);
}
}
bool p[16], col[40][40];
const int dx[4] = {0, 2, 0, -2};
const int dy[4] = {2, 0, -2, 0};
void dfs(int x, int y) {
col[x][y] = true;
for (int i = 0, tx, ty; i < 4; i++) {
tx = x + dx[i]; ty = y + dy[i];
int midx = x + dx[i] / 2, midy = y + dy[i] / 2;
if (tx >= n || tx < 0 || ty < 0 || ty >= m || col[tx][ty]) continue;
bool fl = false;
for (int j = 0; j < k; j++) {
if (!(wall >> j & 1)) continue;
if (x1[j] == x2[j]) {
if (midx == x1[j] && midy >= y1[j] && midy <= y2[j]) {
fl = true;
break;
}
}
else {
if (midy == y1[j] && midx >= x1[j] && midx <= x2[j]) {
fl = true;
break;
}
}
}
if (!fl) dfs(tx, ty);
}
return;
}
int search() {
int ans = k, tn = 1 << k;
for (int i = 0; i < tn; i++) {
if (k - __builtin_popcount(i) > ans) continue;
memset(col, 0, sizeof col);
wall = i;
// cout << "wall:"<<wall<<endl;
dfs(xs, ys);
if (col[xt][yt]) {
if (k - __builtin_popcount(i) < ans) ans = k - __builtin_popcount(i);
}
}
return ans;
}
int main() {
for (int T = _read(); T; T--) {
init();
_write(search());
putchar('\n');
}
return 0;
}
C. backpack
bitset优化背包DP
题意
求恰好装满背包的时候, 异或值最大值。
数据范围
\(T\leq 10\)
\(1\leq n,m<2^{10}\)
\(1\leq v_i,w_i < 2^{10}\)
思路
- 首先要考虑暴力dp如何设计:\(f[i][j][k]\) 表示到物品 i,异或值为 j,体积为 k 的方案是否存在。
- \(f[i][j][k] = f[i-1][j][k] + f[i-1][j\bigoplus w][k - v]\)
- 如果正常来做,空间可以滚动,但是时间复杂度好像炸了是 \(O(2^{30})\)
- 如果有经验的话,可以用 bitset 来优化体积的转移,因为是加减。整体复杂度除 \(64\) 。最后算下来是 \(O(2^{30}/64)\),可以通过。
- 转移就是
-
\[f_{i,j,k} = f_{i-1,j,k}\;|\;f_{i-1,j\bigoplus w_i,k<<v_i} \]
-
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define fi first
#define se second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const int N = 1030;
bitset<1030> f[N], g[N];
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
int n, m;
cin >> n >> m;
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
int v, w;
cin >> v >> w;
for (int j = 0; j < 1024; j++) {
g[j] = f[j];
g[j] <<= v;
}
for (int j = 0; j < 1024; j++) {
f[j] |= g[j ^ w];
}
}
int ans = -1;
for (int j = 0; j < 1024; j++) {
if (f[j][m]) {
ans = j;
}
}
cout << ans << endl;
for (int i = 0; i <= 1024; i++)
f[i].reset(), g[i].reset();
}
return 0;
}
D. Ball
bitset, 枚举
题意
给了 \(n\) 个点的坐标和大小为 \(m*m\) 的方格上,任意三元组之间 3 个曼哈顿距离的中位数是质数的三元组个数。
数据范围
\(1\leq N\leq 2000\)
\(1\leq M\leq 10^5\)
思路
- 题意翻译成人话,对三元组 \((a,b,c)\) 假设 \(l_{ac}\geq l_{ab}\geq l_{bc}\) ,\(l_{ab}\) 是质数的个数。
- 先筛掉 \(2e5\) 内的质数,\(N\leq 2000\),显然可以考虑枚举,\(n^3\) 的枚举显然寄了,考虑怎么优化呢?
- 我们可以在 \(O(n^2)\) 复杂度求得,每个点之间的距离,得到邻接矩阵。
- 对于每条长度为素数的边,我们考虑如何找对应的答案,需要找一条比它小的和比它大的。
- 对于比较乱的序列,一个习惯是先排序,从小到大枚举,先出现的边显然比当前边小,怎么找比当前边大的并且端点符合要求呢?
- 这其实有一点像
bitset
优化找三元环的过程,这里给出一道题目提供练习,ABC258_G。 - 然而这里的长度是大于 1 的整数,但我们可以通过从小到大的枚举边的方式,把长度变为 0 或者 1,代表比当前边大还是小。方式其实无所谓,但初始化为 0 要方便一些。
- 所以开 \(n\) 个长度为 \(n\)
bitset bt
替代邻接矩阵,\(bt[i][j]=1\) 代表 \(l_{ij}\) 比当前边小,否则大,枚举完一条边后更新邻接矩阵。 - 对于每个长度为素数的边 \(l_{ab}\) 的答案就是
bt[a]^bt[b]
中 1 的数量。而因为从小到大枚举答案记录是不重不漏的,可以考虑 \(l_{ac}\geq l_{ab}\geq l_{bc}\) 三条边相同连续被遍历,但是我们统计答案并不会有重复
- 时间复杂度 \(O(n^3/64)\) ,\(64\) 是 bitset 的分母常数根据设备而定。
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define get_sz(v) (int)v.size()
#define fi first
#define se second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const int N = 2010, M = 2e5 + 10;
int primes[M], cnt; // primes[]存储所有素数
bool st[M]; // st[x]存储x是否被筛掉
bitset<2010> bt[N];
struct P {
int a, b, c;
bool operator < (const P& o) const {
return c < o.c;
}
}v[N * N];
void get_primes(int n) {
for (int i = 2; i <= n; i ++ ) {
if (st[i]) continue;
primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i)
st[j] = true;
}
}
int main() {
int T;
scanf("%d", &T);
st[1] = true;
get_primes(M - 9);
while (T--) {
int n, m;
scanf("%d%d", &n, &m);
vector<PII> a(n);
for (int i = 0; i <= n; i++)
bt[i].reset();
for (auto & x : a)
scanf("%d%d", &x.fi, &x.se);
int cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int dist = abs(a[i].fi - a[j].fi) + abs(a[i].se - a[j].se);
v[cnt++] = (P){i + 1, j + 1, dist};
}
}
sort(v, v + cnt);
ll ans = 0;
for (int i = 0; i < cnt; i++) {
auto t = v[i];
if (!st[t.c])
ans += (bt[t.a] ^ bt[t.b]).count();
bt[t.a][t.b] = 1, bt[t.b][t.a] = 1;
}
printf("%lld\n", ans);
}
return 0;
}
H. Path (未补)
I. Laser
思维枚举、计算几何、放缩坐标
题意
K. Random
签到直接输入 \((n-m)/2\) 即可,需要求下 \(2\) 的逆元。
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define fi first
#define se second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const int mod = 1e9 + 7;
ll qmi(ll a, ll k, int mod) {
ll res = 1;
while (k) {
if (k & 1)
res = res * a % mod;
a = a * a % mod;
k >>= 1;
}
return res;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
int n, m;
cin >> n >> m;
cout << (n - m) * qmi(2, mod - 2, mod) % mod << endl;
}
return 0;
}
L. Alice and Bob
博弈、思维、按位赋权
题意
Alice 每次将所有数分成两个集合, Bob 每次删除一个集合,剩下的集合整体减 1。如果任何时候集合中的数有 \(0\) Alice 赢, 如果 Bob 把数删光了,则 Bob 赢。
输入是 \(n + 1\) 个值为 \(i\) 的个数
数据范围
\(\Sigma a_i \leq 1e6\)
\(n \leq 1e6\)
思路
- 多模拟一下可以发现,对于某个值 \(x\) 有 \(2^x\) 个这个数的话,Alice 是必赢的。比如 1 个 0 或 2 个 1 或 4 个 2。从 Alice 的角度思考问题。
- 其实 1 个 1 和 2 个 2 也能赢,先把 1 给分开就赢了。
- 再试试又发现从 1 开始连续的 \(n\) 个数,其中任意一个数有 2 个就能赢。
- 有更正面的考虑方法。考虑问题时,如果一边有一个 \(1\) ,那这个 \(1\) 是必删的,如果有 2 个 \(2\) 那这两个 \(2\) Bob 是必删的,如果有 4 个 \(3\) 那也是必删的。剩余的数会减 1 ,那其实意味着要赢的话,他们所需要的个数减少了一半。
- 如果从前往后做,记录
sum
,到每一位 \(i\) 判断 \(sum + a[i] \geq 2^i\) 大于就赢。否则sum = 2 * (sum + a[i])
。这样做一定能行,但是做不了,因为存不下。 - 有更 smart 的方式是倒着做,这其实实际上是对每一位赋予权重 \(1/{2^x}\) ,最后权重之和大于 \(1\) Alice 就能赢。具体见代码。
- 时间复杂度 \(O(n)\)
Solution
#include<bits/stdc++.h>
#define fi first
#define se second
#define arr(x) x.begin(),x.end()
#define endl '\n'
using namespace std;
typedef std::pair<int, int> PII;
typedef long long ll;
const int mod = 1e9 + 7;
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<ll> a(n + 1, 0);
for (int i = 0; i <= n; i++)
cin >> a[i];
bool fl = false;
for (int i = n - 1; i >= 0; i--)
a[i] += a[i + 1] / 2;
(a[0] > 0) ? cout << "Alice\n" : cout << "Bob\n";
}
return 0;
}
总结
榜单情况
官方榜
校内榜
就是那个 1012 WA 5 的
Problem A
被 sb 杭电卡递归栈空间,改手写栈过了。。。
Problem B
队友读错题,最后一小时没改回来,主要还是博弈题没过,不然一起看B还是能过的,半小时不到就补完了。
Problem C
赛时想了有半个钟头,实在没啥思路,std是bitset优化,目前还不是很理解。
Problem I
Problem K
签到,不解释。
Problem L
博弈题,比赛十分钟出了思路给翔哥讲了下,感觉很正确,一直以为是对的,最后两小时交发现WA了,然后在犹豫改A题单调栈,一直徘徊,结果两个题都没过,蚌。
ALL
个人
经此一役,以后考虑问题可以先从暴力入手,先想一下能不能奇淫技巧化暴力比如 bitset
然后考虑真正的算法问题。
团队
第一场测试赛,做的比较佛系,总体节奏还是两人一题比较靠谱,一人单开容易寄。