2023年度好题(3)
文章有点长,都是由本人一点一点写出来的,公式加载需要一段时间。
CF576D Flights for Regular Customers
思路
首先我们可以按边的权值对边从小到大进行排序。
然后,我们可以从前往后枚举每一条边 \(i\),假设边 \(i\) 需要经过至少 \(d_i\) 条边才能经过这条边,我们就需要将 \(1\sim i - 1\) 这些边建成一个图然后看一看走 \(d_i\) 步能到达的点有哪些,这是一道经典的矩阵题目,不会的可以看看P2886。
因为我们已经走了 \(d_i\) 条边了,我们可以将边 \(i\) 放入图中,然后我们以从 \(1\) 走 \(d_i\) 步可以到达的点为源点在新图上进行 bfs,看能不能到 \(n\) 点,如果能,那么用 \(dis_n + d_i\) 更新答案,表示我要先走 \(d_i\) 步加入了边 \(i\),再走 \(dis_n\) 步就到了 \(n\)。
同时要注意,这个题目的矩阵乘法不进行优化会 TLE,那么我们怎么优化呢?
看以下代码:
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= n; k++)
res.a[i][j] |= (a.a[i][k] & b.a[k][j]);
我们发现 j
与 a.a[i][k]
毫无关系,所以当 a.a[i][k]
成立时,整个 b.a[k]
都可以与 res.a[i]
进行异或,所以这一步就交给 bitset
来做就好了。
for (int i = 1; i <= n; i++)
for (int k = 1; k <= n; k++)
if (a.a[i][k])
res.a[i] |= b.a[k];
代码
/*******************************
| Author: SunnyYuan
| Problem: Flights for Regular Customers
| Contest: Luogu
| URL: https://www.luogu.com.cn/problem/CF576D
| When: 2023-10-09 14:46:28
|
| Memory: 250 MB
| Time: 4000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 160, INF = 0x3f3f3f3f;
struct matrix {
bitset<N> a[N];
} g, ans;
int n, m;
matrix operator*(matrix a, matrix b) {
matrix res;
memset(res.a, 0, sizeof(res.a));
for (int i = 1; i <= n; i++)
for (int k = 1; k <= n; k++)
if (a.a[i][k])
res.a[i] |= b.a[k];
return res;
}
void pow(matrix& ans, matrix a, int b) {
while (b) {
if (b & 1) ans = ans * a;
a = a * a;
b >>= 1;
}
}
struct edge {
int u, v, w;
} e[N];
int d[N];
queue<int> q;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= m; i++) cin >> e[i].u >> e[i].v >> e[i].w;
sort(e + 1, e + m + 1, [](const edge e1, const edge e2) { return e1.w < e2.w; });
memset(ans.a, 0, sizeof(ans.a));
for (int i = 1; i <= n; i++) ans.a[i][i] = 1;
int pass = 0, res = INF;
for (int i = 1; i <= m; i++) {
int u = e[i].u, v = e[i].v, w = e[i].w;
int take = w - pass;
pow(ans, g, take);
g.a[u][v] = 1;
memset(d, 0x3f, sizeof(d));
for (int i = 1; i <= n; i++){
if (ans.a[1][i]) d[i] = 0, q.push(i);
}
while (q.size()) {
int t = q.front();
q.pop();
for (int i = 1; i <= n; i++)
if (g.a[t][i] && d[i] == INF) {
d[i] = d[t] + 1;
q.push(i);
}
}
pass = w;
res = min(res, pass + d[n]);
}
if (res == INF) cout << "Impossible\n";
else cout << res << '\n';
return 0;
}
CF915D Almost Acyclic Graph
思路
非常有意思的一道拓扑排序题。
如果图中没有环,那么在拓扑排序后所有的入度都小于等于 \(0\)。
如果去掉一条边 \((u, v)\),那么就相当于 \(v\) 的入度 \(-1\)。
所以我们相当于枚举 \(i = 1\sim n\),将 \(i\) 的入度 \(-1\) 并且重新跑一遍拓扑排序,如果入度都小于等于 \(0\),那么输出证明确实能够去掉最多一条边就让这个图无环。
代码
/*******************************
| Author: SunnyYuan
| Problem: Almost Acyclic Graph
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/CF915D
| When: 2023-10-13 17:34:10
|
| Memory: 250 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
int n, m;
vector<int> e[N];
int in[N], back[N];
bool topsort() {
memcpy(in, back, sizeof(in));
queue<int> q;
for (int i = 1; i <= n; i++) if (!in[i]) q.push(i);
while (q.size()) {
int t = q.front();
q.pop();
for (int to : e[t]) {
in[to]--;
if (!in[to]) q.push(to);
}
}
for (int i = 1; i <= n; i++) if (in[i] > 0) return false;
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
e[u].push_back(v);
back[v]++;
}
if (topsort()) {
cout << "YES\n";
return 0;
}
for (int i = 1; i <= n; i++) {
if (back[i] <= 0) continue;
back[i]--;
if (topsort()) {
cout << "YES\n";
return 0;
}
back[i]++;
}
cout << "NO\n";
return 0;
}
P4816 [USACO15DEC] High Card Low Card G
思路
贪心题,对于 \(a\) 数组的前一半,要战胜 \(a_i\),那么就要选择一个 \(b_j > a_i\)。那么我们先将 \(a\) 的前一半从大到小排序,然后丢出 Bessie 有的最大值 \(\max\),如果 \(\max < a_i\),Bessie 就输了就继续试探 \(a_{i - 1}\),否则 Bessie 赢了。
对于 \(a\) 的后面,直接将上面的做法反过来即可。
代码
/*******************************
| Author: SunnyYuan
| Problem: P4816 [USACO15DEC] High Card Low Card G
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P4816
| When: 2023-10-13 11:09:37
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, a[N], s[N];
bool st[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], s[a[i]] = 1;
sort(a + 1, a + 1 + n / 2, greater<int>());
sort(a + 1 + n / 2, a + n + 1);
int ans = 0;
int l = 1, r = n << 1;
for (int i = 1; i <= (n >> 1); i++) {
while (r >= 1 && s[r]) r--;
if (r < a[i]) continue;
ans++;
r--;
}
for (int i = 1; i <= (n >> 1); i++) {
while ((l <= (n << 1)) && s[l]) l++;
if (l > a[i + (n >> 1)]) continue;
ans++;
l++;
}
cout << ans << '\n';
return 0;
}
P3203 [HNOI2010] 弹飞绵羊
思路
对于每一个元素 \(x\) 处理它跳出当前块到达的位置 \(p_x\) 与步数 \(step_x\)。
怎么更新位置 \(x\) 呢?
如果 \(x + a_x\) 还在同一块内,那么 \(p_x = p_{x + a_x}, step_x = step_{x + a_x} + 1\)。
如果 \(x + a_x\) 不在同一块内,那么 \(p_x = x + a_x, step_x = 1\)。
对于询问,我们不断让 \(ans\) 加上 \(step_x\),然后 \(x\) 跳到 \(p_x\) 继续加,直到 \(x > n\) 时停止。
代码
/*******************************
| Author: SunnyYuan
| Problem: P3203 [HNOI2010] 弹飞绵羊
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P3203
| When: 2023-10-14 20:04:37
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, c, a[N];
int p[N], s[N];
int L[N], R[N];
int sy[N];
int m;
int query(int x) {
int ans = 0;
while (x <= n) {
ans += s[x];
x = p[x];
}
return ans;
}
void modify(int x, int v) {
a[x] = v;
int j = sy[x];
for (int i = R[j]; i >= L[j]; i--) {
int nxt = i + a[i];
if (nxt > R[j]) p[i] = nxt, s[i] = 1;
else p[i] = p[nxt], s[i] = s[nxt] + 1;
sy[i] = j;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
c = sqrt(n);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= c; i++) {
L[i] = R[i - 1] + 1;
R[i] = L[i] + c - 1;
}
if (R[c] != n) {
c++;
L[c] = R[c - 1] + 1;
R[c] = n;
}
for (int j = c; j >= 1; j--) {
for (int i = R[j]; i >= L[j]; i--) {
int nxt = i + a[i];
if (nxt > R[j]) p[i] = nxt, s[i] = 1;
else p[i] = p[nxt], s[i] = s[nxt] + 1;
sy[i] = j;
}
}
cin >> m;
for (int i = 1, opt, a, b; i <= m; i++) {
cin >> opt;
if (opt == 1) {
cin >> a;
a++;
cout << query(a) << '\n';
}
else {
cin >> a >> b;
a++;
modify(a, b);
}
}
return 0;
}
HDU-5057 Argestes and Sequence
思路
将每个数字拆成很多位并分别进行统计。
非常适合分块题。
单点修改:\(O(1)\)。
区间询问:\(O(\sqrt n)\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: Argestes and Sequence
| OJ: Virtual Judge - HDU
| URL: https://vjudge.net/problem/HDU-5057#author=634579757
| When: 2023-10-14 20:34:27
|
| Memory: 32 MB
| Time: 2500 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 100010, V = 510;
int n, m, c;
int a[N];
int L[V], R[V], p[N];
int sum[V][16][10];
i64 p10[17];
int query(int l, int r, int x, int v) {
int p1 = p[l], p2 = p[r];
if (p1 == p2) {
int cnt = 0;
for (int i = l; i <= r; i++) {
cnt += ((a[i] / p10[x - 1]) % 10) == v;
}
return cnt;
}
else {
int cnt = 0;
for (int i = l; i <= R[p1]; i++) {
cnt += ((a[i] / p10[x - 1]) % 10) == v;
}
for (int i = L[p2]; i <= r; i++) {
cnt += ((a[i] / p10[x - 1]) % 10) == v;
}
for (int i = p1 + 1; i < p2; i++) {
cnt += sum[i][x][v];
}
return cnt;
}
}
void modify(int u, int x) {
for (int i = 1; i <= 15; i++) {
sum[p[u]][i][a[u] % 10]--;
a[u] /= 10;
}
a[u] = x;
for (int i = 1; i <= 15; i++) {
sum[p[u]][i][x % 10]++;
x /= 10;
}
}
void solve() {
memset(sum, 0, sizeof(sum));
memset(L, 0, sizeof(L));
memset(R, 0, sizeof(R));
cin >> n >> m;
c = sqrt(n);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= c; i++) {
L[i] = R[i - 1] + 1;
R[i] = L[i] + c - 1;
}
if (R[c] != n) {
c++;
L[c] = R[c - 1] + 1;
R[c] = n;
}
for (int j = 1; j <= c; j++) {
for (int i = L[j]; i <= R[j]; i++) {
int x = a[i];
for (int k = 1; k <= 15; k++) {
sum[j][k][x % 10]++;
x /= 10;
}
p[i] = j;
}
}
char opt;
int a, b, c, d;
for (int i = 1; i <= m; i++) {
cin >> opt;
if (opt == 'Q') {
cin >> a >> b >> c >> d;
cout << query(a, b, c, d) << '\n';
}
else {
cin >> a >> b;
modify(a, b);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
p10[0] = 1;
for (int i = 1; i < 16; i++) p10[i] = p10[i - 1] * 10;
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF1886D. Monocarp and the Set
建议标签:数学。
建议难度:普及/提高-。
思路
这个题目非常有意思。
当您苦思冥想都想不出来怎么正着做的时候,不妨把问题反过来想想。
题中说是加数,我们就可以把所有的操作反过来,从 \(n\) 个数字中不断删除数字。
首先如果是 \(>\),那么就只能删除最大值,有一种选法。
其次如果是 \(<\),那么只能删除最小值,也只有一种选法。
当操作为 \(?\) 时,那么既不删除最大值,也不删除最小值,有 \(len - 2\) 种选法,\(len\) 是数组长度。
因为当第 \(i\) 次操作完的时候有 \(i\) 个数字,去除最大最小值,答案就是所有满足 \(s_i = ?\) 的 \((i - 2)\) 的乘积。
对于每一个询问,如果要将是问号的一位改成别的(\(s_i = '?' \rightarrow s_i = >/<\)),那么就要除以 \((i - 2)\),这个可以使用逆元实现,不会的可以去补一补数论。当然,如果是一个别的字符改成问号也要记得乘上 \((i - 2)\)。
还要特判,如果在只有两个数字的时候有 \(?\),那不可能完成!因为除了最大值就是最小值,所以要直接输出 \(0\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: D. Monocarp and the Set
| Contest: Codeforces - Educational Codeforces Round 156 (Rated for Div. 2)
| URL: https://codeforces.com/contest/1886/problem/D
| When: 2023-10-11 14:41:49
|
| Memory: 256 MB
| Time: 2000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 300010, mod = 998244353;
int n, m, ans;
int modfs[N];
char s[N];
int pow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1ll * res * a % mod;
a = 1ll * a * a % mod;
b >>= 1;
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> (s + 2);
for (int i = 1; i <= n; i++) modfs[i] = pow(i, mod - 2);
ans = 1;
for (int i = 3; i <= n; i++)
if (s[i] == '?')
ans = 1ll * ans * (i - 2) % mod;
if (s[2] == '?') cout << "0\n";
else cout << ans << '\n';
int p;
char x;
while (m--) {
cin >> p >> x;
p++;
if (s[p] == '?' && p > 2) ans = 1ll * ans * modfs[p - 2] % mod;
if (x == '?' && p > 2) ans = 1ll * ans * (p - 2) % mod;
s[p] = x;
if (s[2] == '?') cout << "0\n";
else cout << ans << '\n';
}
return 0;
}
AcWing 878. 线性同余方程
题目描述
思路
转化一下:
现在已知 \(a, m\),要求 \(x, p\),可以使用扩展欧几里得算法,不会的可以参考我的博客。
求出来的 \(b\) 还必须是 \(\gcd(a, m)\) 的倍数才可以(裴蜀定理),否则无解。
如果有解记得将 \(x\) 乘上 \(\dfrac{b}{\gcd(a, m)}\)。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int exgcd(int a, int b, int&x, int &y) {
if (!b) {
x = 1, y = 0;
return a;
}
int g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
void solve() {
int a, b, m;
cin >> a >> b >> m;
int x, y;
int g = exgcd(a, m, x, y);
if (b % g) cout << "impossible\n";
else cout << (i64)x * b / g % m << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
AcWing 104. 货仓选址
题目描述
思路
假设我们选点 \(x\),那么题目要求最小化:
我们对等式进行化简:
假设 \(A_i \le A_{i + 1}\) 对于所有 \(i < n\)。
那么到这里大家已经可以看出来了,实际上是取所有数值的中位数就可以让上面的 \(\ge\) 变成 \(=\) 以达到最小化的目的。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N], n;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
int pos = a[(n + 1) / 2], ans = 0;
for (int i = 1; i <= n; i++) ans += abs(a[i] - pos);
cout << ans << '\n';
return 0;
}
CF979C Kuro and Walking Route
思路
其实本题很简单,只要将树的根换为 \(x\),然后答案就是 \(n \times (n - 1)\) 减去这两个 \(size\) 相乘,即所有的方案减去这两个被我圈出来的部分的点两两组合的个数。
第一个 \(size_1\) 就是原本的 \(size_x\) 减去 \(y\) 所在的子树的 \(size_v\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 300010, M = 600010;
struct edge {
int to, next;
} e[M];
int head[N], idx;
void add(int u, int v) {
idx++, e[idx].to = v, e[idx].next = head[u], head[u] = idx;
}
int n, x, y;
int sz[N];
bool dfs(int u, int fa) {
bool flag = (u == y);
sz[u] = 1;
for (int i = head[u]; i; i = e[i].next) {
int to = e[i].to;
if (to == fa) continue;
bool d = dfs(to, u);
if (!d) sz[u] += sz[to];
flag |= d;
}
return flag;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >>n >> x >> y;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs(x, 0);
cout << 1ll * n * (n - 1) - 1ll * sz[x] * sz[y] << '\n';
return 0;
}
P9745 「KDOI-06-S」树上异或
前言
借鉴了 xyzfrozen 题解的思路,想在这里详细讲一讲。
还有大家好像都不爱画图,看得挺费劲。
LaTeX 公式中的 \(u, v\) 很像,请注意甄别。
思路
首先设 \(f_{i, j, k}\) 在以 \(i\) 为根的子树中,对于每一种割边的方法,点 \(i\) 所在连通块异或出来的值在二进制表示下的第 \(j\) 位为 \(k\) 的情况下,其他连通块的异或的乘积之和。
比如下图的割边方式:
\(f_{i, j, k}\) 就记录着所有割边情况的乘积之和。
介绍好了状态,我们想一想怎么转移,对于每一条边 \((u, v)\),假设 \(u\) 是 \(v\) 的父亲节点。
因为我们要从上一个状态递推到这个状态,所以先将 \(f_{u}\) 拷贝到一个临时数组 \(g\),即 \(g_{i, j} = f_{u, i, j}\),每次枚举新边的时候重新拷贝。
转移要分类讨论:
- 首先,我们可以让 \(v\) 所在连通块并入 \(u\) 所在的连通块:
对于将 \(v\) 并入 \(u\) 的部分,并且 \(v\) 所在连通块的异或和为 \(l\),\(u\) 所在连通块的异或和为 \(k\):
- 然后,我们也可以让 \(v\) 还是一个独立的连通块,不要并入 \(u\)。
我们可以计算出 \(v\) 的贡献:\(t = \sum\limits_{i = 0}^{63}2^if_{v,i,1}\)。
然后:
我认为我讲的比较详细了,如果还不懂,建议先做一做P2015 二叉苹果树。
代码
非常详细的注释,大家可以慢慢看。
/*******************************
| Author: SunnyYuan
| Problem: P9745 「KDOI-06-S」树上异或
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P9745
| When: 2023-10-20 12:10:24
|
| Memory: 512 MB
| Time: 2000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 500010, mod = 998244353;
vector<int> e[N]; // 建图
i64 n, x[N]; // 存储信息
int f[N][64][2]; // DP
int pow2[64]; // pow2[i] 保存 2 的 i 次幂
void dfs(int u, int fa) {
int tmp[64][2]; // 保存当前状态
for (int to : e[u]) { // 遍历每一条边
if (to == fa) continue;// 是父节点,不退回去
dfs(to, u); // 走到子节点
memcpy(tmp, f[u], sizeof(tmp));// 保存当前状态
memset(f[u], 0, sizeof(f[u]));// 清空重新计算
int ans_v = 0; // 计算 to 所在子树可以给的贡献
for (int j = 0; j < 64; j++) {// 对于每一位计算贡献(ans_v)
(ans_v += 1ll * f[to][j][1] * pow2[j] % mod) %= mod;
}
for (int j = 0; j < 64; j++) {// 现在正在计算第 j 位的贡献
for (int k = 0; k < 2; k++) {// 枚举 u 的第 j 位是 0 还是 1
(f[u][j][k] += 1ll * ans_v * tmp[j][k] % mod) %= mod;// u 不让 to 所在连通块并进来
for (int x = 0; x < 2; x++) {
(f[u][j][k ^ x] += 1ll * tmp[j][k] * f[to][j][x] % mod) %= mod;// u 让 to 所在连通块并进来,所以我们要枚举 to 的第 j 位是 0 还是 1
}
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
pow2[0] = 1; // 2 的 0 次幂是 1
for (int i = 1; i < 64; i++) (pow2[i] = pow2[i - 1] + pow2[i - 1]) %= mod; // 初始化 2 的 i 次幂
cin >> n; // 点数
for (int i = 1; i <= n; i++) cin >> x[i];// 输入点权
for (int i = 2; i <= n; i++) {// 输入每一条边
int to;
cin >> to;
e[to].push_back(i); // 建立双向边
e[i].push_back(to);
}
for (int i = 1; i <= n; i++)// 第 i 个数字
for (int j = 0; j < 64; j++)// 第 j 位
f[i][j][x[i] >> j & 1] = 1;// 初始化 x[i]
dfs(1, 0); // 树形 dp
i64 ans = 0; // 答案
for (int j = 0; j < 64; j++) {// 统计答案
(ans += 1ll * f[1][j][1] * pow2[j] % mod) %= mod;
}
cout << ans << '\n'; // 输出答案
return 0;
}
P1441 砝码称重
思路
这道题目是 dfs + dp 的练手好题。
首先我们可以使用 dfs 从 \(n\) 中挑选 \(m\) 个。
然后做一个类似 01 背包的东西统计个数。
设 \(f_i\) 表示若干个砝码堆出重量 \(i\) 的方案数。
对于每一个选择的物体 \(i\):
代码
/*******************************
| Author: SunnyYuan
| Problem: P1441 砝码称重
| Contest: Luogu
| URL: https://www.luogu.com.cn/problem/P1441
| When: 2023-10-03 23:45:38
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 25, M = 2010;
int n, m, a[N], f[M];
int sum;
bool choose[N];
int cnt;
bool c[M];
int ans;
void dfs(int u) {
if (cnt > m) return;
if (u > n) {
if (cnt < m) return;
memset(f, 0, sizeof(f));
memset(c, 0, sizeof(c));
f[0] = 1;
for (int i = 1; i <= n; i++) {
if (choose[i]) continue;
for (int j = M - 1; j >= a[i]; j--) f[j] += f[j - a[i]];
}
for (int j = 1; j < M; j++) {
if (f[j]) c[j] = true;
}
sum = 0;
for (int j = 1; j < M; j++) sum += c[j];
ans = max(ans, sum);
return;
}
cnt++;
choose[u] = true;
dfs(u + 1);
cnt--;
choose[u] = false;
dfs(u + 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
dfs(1);
cout << ans << '\n';
return 0;
}
CF1407D Discrete Centrifugal Jumps
思路
首先,不难想到,是这两种情况才可以转移:
然后我们可以维护两个单调栈:
对于这两种情况,我们要仔细想一想怎么从之前的一个 \(i\) 转移到 \(j\) 了,注意是从 \(i\) 转移到 \(j\)。
首先,我们想一想,现在我们来到了 \(j\)。
看下图:是不是栈中任何一个满足 \(h_k < h_j\),都可以将 \(k\) 到栈顶的所有元素作为下凸的那一个部分,然后 \(k\) 前面的一个作为 \(i = k - 1\) 可以转移到 \(j\):
实际上,我们想让 \(j\) 离得越远越好,所以可以边弹出边转移,直到 \(a_k > a_i\) 的时候停止。
第二种上凸的情况就不说了,就把上面的东西全部反过来就可以了。
代码
/*******************************
| Author: SunnyYuan
| Problem: Discrete Centrifugal Jumps
| Contest1: Luogu
| URL: https://www.luogu.com.cn/problem/CF1407D
| When: 2023-10-05 19:39:46
|
| Memory: 250 MB
| Time: 2000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 300010;
int n, h[N], f[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> h[i];
stack<int> s1, s2;
f[0] = -1;
for (int i = 1; i <= n; i++) {
f[i] = f[i - 1] + 1;// 跳一步
while (s1.size() && h[s1.top()] <= h[i]) { // 第一种情况
int x = s1.top();
s1.pop();
if (s1.size() && min(h[s1.top()], h[i]) > h[x]) f[i] = min(f[i], f[s1.top()] + 1);
}
while (s2.size() && h[s2.top()] >= h[i]) { // 第二种情况
int x = s2.top();
s2.pop();
if (s2.size() && max(h[s2.top()], h[i]) < h[x]) f[i] = min(f[i], f[s2.top()] + 1);
}
s1.push(i);
s2.push(i);
}
cout << f[n] << '\n';
return 0;
}
CF1614D1 Divan and Kostomuksha (easy version)
前言
基本思路,zltqwq 大佬已经讲的很清楚了,我想补充一下怎么转移 DP。
被 long long
卡了 6 发。
思路
先说说基本思路。
首先我们计算出 \(c_i\) 表示所有数字中有因数 \(i\) 的数字个数,设 \(f_i\) 表示以 \(i\) 为公因数时可以贡献的和(不包括不能整除 \(i\) 的)。
最开始时 \(f_i = c_i \cdot i\)。
转移方程为 \(f_{i} = f_{i\cdot p} + i\cdot (c_i - c_{i \cdot p})\)。
这表示啥呢?这个实际上是先将 \(c\cdot p\) 的倍数放在前面,然后将 \(c_{i} - c_{i\cdot p}\) 个数字放在后面,因为 \(i\) 的倍数中包含 \(i\cdot p\) 的倍数,所以要减掉,这样保证了前面的 \(\gcd\) 是 \(c\cdot p\),后面的是 \(\gcd\) 是 \(c\),目的就是让和最大化。
最后的答案就是 \(ans = \max\limits_{c_i = n} f_i\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: Divan and Kostomuksha (easy version)
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/CF1614D1
| When: 2023-10-20 20:25:20
|
| Memory: 1000 MB
| Time: 4000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 100010, M = 5000010;
int prime[M], cnt;
bool inp[M];
void getprime() {
inp[1] = inp[0] = 1;
for (int i = 2; i < M; i++) {
if (!inp[i]) prime[++cnt] = i;
for (int j = 1; j <= cnt && prime[j] * i < M; j++) {
inp[prime[j] * i ] = true;
if (i % prime[j] == 0) break;
}
}
}
int n, a[N];
i64 f[M];
int c[M];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
getprime();
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= a[i] / j; j++) {
if (a[i] % j == 0) {
c[j]++;
if (a[i] / j != j) c[a[i] / j]++;
}
}
}
for (int i = 0; i < M; i++) f[i] = 1ll * c[i] * i;
for (int i = M - 1; i >= 1; i--) {
for (int j = 1; j <= cnt && 1ll * i * prime[j] < M; j++) {
f[i] = max(f[i], f[i * prime[j]] + 1ll * i * (c[i] - c[i * prime[j]]));
}
}
i64 ans = 0;
for (int i = 0; i < M; i++)
if (c[i] == n)
ans = max(ans, f[i]);
cout << ans << '\n';
return 0;
}
CF915E Physical Education Lessons
思路
这道题目看起来非常 ODT,但是显然我们可以用动态开点的方法来解决这个题目。
维护 l, r
表示左右儿子。
维护 tag
表示是否会修改区间。
维护 sum
表示该区间不是工作日的和,最后的答案就是 n - sum
。
代码
然后今天很晚了,懒得再写 pushup, pushdown, addtag
了,直接全部写在一个函数里面了。
然后空间能开多大开多大,然后就卡过了,然后就没有然后了。
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 15000000;
struct node {
int l, r;
int tag, sum;
} tr[SIZE];
int idx = 1, root = 1;
int n, q;
void modify(int u, int l, int r, int pl, int pr, int k) {
if (pl <= l && r <= pr) {
if (k == 1) tr[u].tag = 1, tr[u].sum = r - l + 1;
else tr[u].tag = 2, tr[u].sum = 0;
return;
}
int mid = l + r >> 1;
if (tr[u].tag) {
if (!tr[u].l) tr[u].l = ++idx;
if (!tr[u].r) tr[u].r = ++idx;
tr[tr[u].l].tag = tr[u].tag;
tr[tr[u].r].tag = tr[u].tag;
if (tr[u].tag == 1) tr[tr[u].l].sum = mid - l + 1, tr[tr[u].l].tag = 1;
else tr[tr[u].l].sum = 0, tr[tr[u].l].tag = 2;
if (tr[u].tag == 1) tr[tr[u].r].sum = r - mid, tr[tr[u].r].tag = 1;
else tr[tr[u].r].sum = 0, tr[tr[u].r].tag = 2;
tr[u].tag = 0;
}
if (pl <= mid) {
if (!tr[u].l) tr[u].l = ++idx;
modify(tr[u].l, l, mid, pl, pr, k);
}
if (pr > mid) {
if (!tr[u].r) tr[u].r = ++idx;
modify(tr[u].r, mid + 1, r, pl, pr, k);
}
tr[u].sum = tr[tr[u].l].sum + tr[tr[u].r].sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> q;
for (int i = 1, l, r, k; i <= q; i++) {
cin >> l >> r >> k;
modify(root, 1, n, l, r, k);
cout << n - tr[1].sum << '\n';
}
return 0;
}
P1052 [NOIP2005 提高组] 过河
思路
设 \(f_i\) 表示到 \(i\) 需要踩到的最小石头数量,\(r_i\) 表示第 \(i\) 个位置有没有石头,有:
但是这个会 TLE,因为 \(L\) 有 \(10^9\),所以如果相邻两个石子之间的距离大于 100,我们就可以把它变成 100,因为有对于互质的两个数字 \(p, q\), 有 \(x \ge 0, y\ge 0\) 使得 \(px + qy > (p - 1)(q - 1) - 1\),不会的可以看看 P3951。我们取 \(p = 10, q = 9\) 就会发现每次走 \(10\) 或 \(9\) 部就可以到达所有 \(72\) 及以上的长度,然后再考虑左右边界可以取 \(100\)。对于长度大于 \(100\) 的,直接将它的距离改称 \(100\) 即可。
同时注意要特判 \(S = T\) 的情况,直接统计 \(r_i \bmod S = 0\) 的情况数量。
代码
/*******************************
| Author: SunnyYuan
| Problem: P1052 [NOIP2005 提高组] 过河
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P1052
| When: 2023-10-25 10:57:04
|
| Memory: 128 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 300000;
int l, s, t, m, a[N], f[M], g[M];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> l >> s >> t >> m;
int K = 1;
for (int i = s; i <= t; i++) K *= i;
for (int i = 1; i <= m; i++) cin >> a[i];
a[++m] = l;
sort(a + 1, a + m + 1);
for (int i = m; i; i--) {
a[i] -= a[i - 1];
int k = a[i] % K;
if (a[i] >= K && k <= t) k += K;
a[i] = k;
}
for (int i = 1; i <= m; i++) a[i] += a[i - 1];
for (int i = 1; i < m; i++) g[a[i]] = 1;
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for (int i = 0; i < M; i++)
for (int j = s; j <= t; j++)
if (i >= j) f[i] = min(f[i], f[i - j] + g[i]);
int ans = 0x3f3f3f3f;
for (int i = a[m]; i < a[m] + t; i++) ans = min(ans, f[i]);
cout << ans << '\n';
return 0;
}
P3243 [HNOI2015] 菜肴制作
思路
首先我们可以想到出度为 \(0\) 的点不会对其他点的顺序产生影响,所以我们就要在所有出度为 \(0\) 的点中选取较小的放在前面,所以这也是大多数题解为什么要建反图并且取最大的字典序的原因。
代码
建反图 + 拓扑排序 + 贪心。
/*******************************
| Author: SunnyYuan
| Problem: P3243 [HNOI2015] 菜肴制作
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P3243
| When: 2023-10-27 19:24:39
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using PII = pair<int, int>;
const int N = 100010;
vector<int> e[N];
int in[N];
int n, m;
void solve() {
for (int i = 1; i <= n; i++) e[i].clear(), in[i] = 0;
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[v].push_back(u);
in[u]++;
}
priority_queue<int, vector<int>, less<int> > q;
for (int i = 1; i <= n; i++) if (!in[i]) q.push(i);
vector<int> ans;
while (q.size()) {
int t = q.top();
ans.push_back(t);
q.pop();
for (int to : e[t]) {
if (!(--in[to])) {
q.push(to);
}
}
}
reverse(ans.begin(), ans.end());
if (ans.size() == n) for (int x : ans) cout << x << ' ';
else cout << "Impossible!";
cout << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF939E Maximize!
借鉴了第一篇题解的思路。
思路
贪心,首先当我们每次执行操作 \(1\) 加入新的数字 \(x\) 的时候一定要取,然后后我们发现还可以从第一个数字开始往后取直到平均值变小。
代码
/*******************************
| Author: SunnyYuan
| Problem: Maximize!
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/CF939E
| When: 2023-10-28 09:05:18
|
| Memory: 250 MB
| Time: 3000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 500010;
i64 sum[N], a[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
int opt, x, cnt = 0, l = 0;
cout << setprecision(10) << fixed;
while (T--) {
cin >> opt;
if (opt == 1) {
cin >> x;
cnt++;
a[cnt] = x;
sum[cnt] = sum[cnt - 1] + x;
}
else {
double res = sum[l] + a[cnt];
while (res / (l + 1) > (res + a[l + 1]) / (l + 2)) {
res += a[l + 1];
l++;
}
cout << a[cnt] - res / (l + 1) << '\n';
}
}
return 0;
}
P2865 [USACO06NOV] Roadblocks G
思路
这是一道经典的求次短路的题目,首先假设 \(dis_{u, 0}\) 表示从起点到 \(u\) 的最短路,\(dis_{u, 1}\) 表示从起点到 \(u\) 的次短路。
假设有边 \((u, v)\),边权为 \(w\),如果 \(dis_{v, 0} > dis_{u, 0} + w\),那么先让 \(dis_{v, 1} \leftarrow dis_{v, 0}\),然后 \(dis_{v, 0} \leftarrow dis_{u, 0} + w\)。
然后,我们还可以只更新次短路而不更新最短路,这有分为两种情况:
- 直接由 \(u\) 的次短路走到 \(v\),但是比 \(dis_{v, 0}\) 大,即如果 \(dis_{u, 1} + w > dis_{v, 0}\) 且 \(dis_{u, 1} + w < dis_{v, 1}\),那么 \(dis_{v, 1} \leftarrow dis_{u, 1} + w\)。
- 直接由 \(u\) 的最短路走到 \(v\),但是比 \(dis_{v, 0}\) 大,即 \(dis_{u, 0} + w > dis_{v, 0}\) 且 \(dis_{u, 0} + w < dis_{v, 1}\),那么 \(dis_{v, 1} \leftarrow dis_{u, 0} + w\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: P2865 [USACO06NOV] Roadblocks G
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P2865#submit
| When: 2023-10-28 15:54:10
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 5010, M = 200010;
struct edge {
int to, next, w;
} e[M];
int head[N], idx = 1;
void add(int u, int v, int w) {
idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
}
int n, m;
int dis[N][2];
bool vis[N][2];
struct node {
int dis, type, id;
bool operator>(const node b) const { return dis < b.dis; }
};
void dijkstra() {
priority_queue<node, vector<node>, greater<node> > q;
memset(dis, 0x3f, sizeof(dis));
dis[1][0] = 0;
q.push({0, 0, 1});
while (q.size()) {
int t = q.top().id, ty = q.top().type;
q.pop();
if (vis[t][ty]) continue;
vis[t][ty] = true;
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (dis[to][0] > dis[t][0] + e[i].w) {
dis[to][1] = dis[to][0];
dis[to][0] = dis[t][0] + e[i].w;
q.push({dis[to][0], 0, to});
q.push({dis[to][1], 1, to});
}
if (dis[to][0] < dis[t][0] + e[i].w && dis[to][1] > dis[t][0] + e[i].w) {
dis[to][1] = dis[t][0] + e[i].w;
q.push({dis[to][1], 1, to});
}
if (dis[to][0] < dis[t][1] + e[i].w && dis[to][1] > dis[t][1] + e[i].w) {
dis[to][1] = dis[t][1] + e[i].w;
q.push({dis[to][1], 1, to});
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w), add(v, u, w);
}
dijkstra();
cout << dis[n][1] << '\n';
return 0;
}
CF1891C. Smilo and Monsters
建议标签:贪心
思路
贪心题,首先我们可以想到可以使用较小的怪物群组合出 \(x\) 然后用 \(1\) 的代价干掉当前最大的怪物群,直到所有较小的怪物加起来都不能干掉最大的怪物时,才要特判。
当所有较小的怪物加起来都不能干掉最大的怪物时,如果当前已经积累了 \(x\) 个怪物,现在只剩下一个最大的怪物群,有 \(t\) 只怪物,那么我们不妨将他们两个加起来考虑。
\(t + x\) 如果是偶数,那么先消灭 \(s = \left\lfloor\dfrac{t + x}{2}\right\rfloor\),然后再用 \(1\) 的代价消灭 \(s\) 只怪物,如果 \(s\) 是奇数,那么还会多 \(1\),所以答案再加 \(1\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: C. Smilo and Monsters
| OJ: Codeforces - Codeforces Round 907 (Div. 2)
| URL: https://codeforces.com/contest/1891/problem/C
| When: 2023-11-01 10:03:36
|
| Memory: 256 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n;
cin >> n;
vector<int> a(n);
for (auto& x : a) cin >> x;
sort(a.begin(), a.end());
int l = 0, r = n - 1, sum = 0;
i64 ans = 0;
while (l <= r) {
while (l < r && sum < a[r]) {
int add = min(a[r] - sum, a[l]);
sum += add;
a[l] -= add;
if (!a[l]) l++;
}
if (sum == a[r]) {
ans += sum + 1;
a[r] = sum = 0;
r--;
}
if (l == r) {
// cout << l << ' ' << r << ' ' << a[l] << endl;
int p = sum + a[l];
if (p & 1) ans += (p / 2) + 1 + (bool)(p / 2);
else ans += (p / 2) + 1;
break;
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
P5098 [USACO04OPEN] Cave Cows 3
我想拓展一下这道题目,而不仅限于是个二维的点,首先说一下基本思路。
思路
题目要求最大化 \(|x_i - x_j| + |y_i - y_j|\)。
分类讨论,如果 \(x_i > x_j\) 且 \(y_i > y_j\) 那么答案就是 \(x_i + y_i - (x_j + y_j)\),那么我们就可以最大化 \(x_i + y_i\),最小化 \(x_j + y_j\),如果 \(x_i > x_j\) 且 \(y_i < y_j\),那么答案就是 \(x_i - y_i - (x_j - y_j)\),那么我们就要最大化 \(x_i - y_i\) 和最小化 \(x_j - y_j\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: P5098 [USACO04OPEN] Cave Cows 3
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P5098
| When: 2023-11-01 11:29:16
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
int max1 = -1e9, max2 = -1e9, min1 = 1e9, min2 = 1e9;
for (int i = 1; i <= n; i++) {
int x, y;
cin >> x >> y;
max1 = max(max1, x + y);
max2 = max(max2, x - y);
min1 = min(min1, x + y);
min2 = min(min2, x - y);
}
cout << max(max1 - min1, max2 - min2) << '\n';
return 0;
}
拓展
我们可以将 2 维拓展到 \(k\) 维。
同样的思路,对于点 \((x_1, x_2, x_3, x_4, x_5, \cdots x_k)\),我们可以先将第一维固定是 \(+\),即 \(x_1\),然后分类讨论后面的维度,它其实是可加可减的,比如,对于 \((a_1, a_2, a_3)\) 和 \((b_1, b_2, b_3)\) 这两个三维的点,先固定 \(a_1 > b_1\),假设 \(a_2 < b_2, a_3 > b_3\),那么就会有
那么我们就要在所有 \(x_1 - x_2 + x_3\) 中选取最大值和最小值并相减。
那么对于每一维 \(a_j\) 和 \(b_j\) 都有两种关系:大于或小于,所以我们对于 \(k\) 维的点,我们可以使用暴搜枚举两两之间的关系,再对每一种关系取 \(\max\),这样可以将复杂度从 \(O(n^2)\) 变为 \(O(2^kn)\),如果 \(k\) 很小这个优化是有效的,比如 \(k \le 4\) 可以写出:
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 INF = 1e16;
struct maxmin {
i64 maxx, minn;
maxmin() { maxx = -INF, minn = INF; }
void update(i64 x) { maxx = max(maxx, x), minn = min(minn, x); }
i64 res() { return maxx - minn; }
} q[8];
int n, k, t[5], cnt;
void dfs(int u, i64 sum) {
if (u > k) {
q[cnt++].update(sum);
return;
}
dfs(u + 1, sum + t[u]);
dfs(u + 1, sum - t[u]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) cin >> t[j];
cnt = 0;
dfs(2, t[1]);
}
i64 res = 0;
for (int i = 0; i < 8; i++) res = max(res, q[i].res());
cout << res << '\n';
return 0;
}
CF958F2 Lightsabers (medium)
思路
设总共有 \(n\) 个数字,值域为 \(m\),\(c_x\) 表示数字 \(x\) 在某一段的个数,\(a\) 表示原数组,\(b_x\) 表示要数字 \(x\) 要达到的个数目标。
首先我们先圈出一段使得所有数字 \(c_x \ge b_x\),那么对于圈出来的这一段我们可以使用 \(\sum\limits_{i = 1}^{m} c_i - b_i\) 的次数删除多余的数字。
我们可以使用双指针完成上述过程,设当前 \((l, r)\) 已经使 \(cnt\) 个数字 \(c_i \ge b_i\),要删除 \(ans\) 个数据才能达到目标,那么固定一个左端点 \(l\),然后只要还没有凑够(\(cnt < m\)),就让 \(r\) 加 \(1\),并且让 \(c_{a_r}\) 加 \(1\),如果 \(c_{a_r} = b_{a_r}\) 就让 \(cnt\) 加 \(1\),如果 \(c_{a_r} > b_{a_r}\),那么说明又多来了一个根本不应该来的家伙,要把它删去,即 \(ans \leftarrow ans + 1\),最后在所有 \(ans\) 中取 \(\min\)。
代码
/*******************************
| Author: SunnyYuan
| Problem: Lightsabers (medium)
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/CF958F2
| When: 2023-11-01 15:20:39
|
| Memory: 250 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, m;
int a[N], b[N], c[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i], c[a[i]]++;
int cnt = 0;
for (int i = 1; i <= m; i++) cin >> b[i], cnt += (b[i] == 0);
memset(c, 0, sizeof(c));
int ans = 0x3f3f3f3f, sum = 0;
for (int l = 1, r = 0; l <= n; l++) {
while (r < n && cnt < m) {
c[a[++r]]++;
if (c[a[r]] == b[a[r]]) cnt++;
if (c[a[r]] > b[a[r]]) sum++;
}
if (cnt == m) ans = min(ans, sum);
else break;
if (c[a[l]] > b[a[l]]) sum--;
c[a[l]]--;
if (c[a[l]] < b[a[l]]) cnt--;
}
if (ans == 0x3f3f3f3f) cout << "-1\n";
else cout << ans << '\n';
return 0;
}
P6902 [ICPC2014 WF] Surveillance
思路
首先,它是一个环,不好处理,我们可以断环为链,所有区域也都复制一遍,我们设 \(f_{i, j}\) 为从 \(i\) 开始走 \(2^j\) 个区域可以到达的最大值,那么有 \(f_{i, j} = f_{f_{i, j - 1} + 1, j - 1}\),即我们可以先从 \(i\) 跳 \(2^{j - 1}\) 步到 \(f_{i, j - 1}\),然后再从 \(f_{i, j - 1} + 1\) 跳 \(2^{j - 1}\) 个区域跳到最大值。
然后,我们对于每一个 \(i\) 试探其要跳到 \(i + n - 1\) 需要的最小步骤 \(step\),我们可以像倍增求 \(LCA\) 一样不断试探,从大到小枚举 \(j\),如果当前位置 \(cur\) 再往前走 \(2^j\) 个区域没有跨过 \(i + n - 1\),即 \(f_{cur + 1, j} < i + n - 1\),那么让 \(cur \leftarrow f_{cur + 1, j}\),试探完成后再让 \(step \leftarrow step + 1\),跨过 \(i + n - 1\)。
最后,我们要谈一谈怎么初始化,这个问题虽然比较小,但是也还是拿出来说一说,就是我们先将所有的区域按左端点排序,接着我们用 \(i\) 从 \(1\) 枚举到 \(n\),然后所有区域中左端点 \(l \le i\) 的区域右端点最大值可以作为 \(f_{i, 0}\),表示其走一步可以到达的最大点,因为枚举的 \(i\) 和区域的左端点都是单调不减的,所以双指针可以在 \(O(n)\) 内完成。
总时间复杂度:\(O(n \log n)\)。
代码
注意:
- 在程序中,为了提速,我将 \(f_{i, j}\) 换成了 \(f_{j, i}\)。
- 如果有一个区域 \(l > r\),那么将 \(r \leftarrow r + n\)。
/*******************************
| Author: SunnyYuan
| Problem: P6902 [ICPC2014 WF] Surveillance
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P6902
| When: 2023-11-01 14:14:05
|
| Memory: 1 GB
| Time: 4000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
using PII = pair<int, int>;
const int N = 2000010, INF = 0x3f3f3f3f, K = 22;
int f[K][N];
int n, m;
PII a[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
int cnt = m;
for (int i = 1; i <= m; i++) {
cin >> a[i].first >> a[i].second;
if (a[i].first > a[i].second) a[i].second += n;
else a[++cnt] = {a[i].first + n, a[i].second + n};
}
sort(a + 1, a + cnt + 1);
int l = 0, maxx = 0;
for (int i = 1; i <= (n << 1); i++) {
while (l + 1 <= cnt && a[l + 1].first <= i) maxx = max(maxx, a[++l].second);
f[0][i] = maxx;
}
for (int j = 1; j < K; j++)
for (int i = 1; i <= (n << 1); i++)
if (f[j - 1][i] + 1 <= (n << 1) && f[j - 1][i])
f[j][i] = f[j - 1][f[j - 1][i] + 1];
int ans = INF;
for (int i = 1; i <= n; i++) {
int k = i - 1, step = 0;
for (int j = K - 1; j >= 0; j--) {
if (f[j][k + 1] < i + n - 1 && f[j][k + 1]) {
k = f[j][k + 1];
step |= (1 << j);
}
}
if (f[0][k + 1] >= i + n - 1) step++;
else step = INF;
ans = min(ans, step);
}
if (ans == INF) cout << "impossible\n";
else cout << ans << '\n';
return 0;
}
P2519 [HAOI2011] problem a
非常有意思的一道紫题。
思路
首先我们发现一个人有 \(x\) 个比他大的和 \(y\) 个比他小的人,那么其名次一定在 \(x + 1 \sim n - y\) 之间。
因此可以计算出他们的名次的范围 \([l, r]\),如果 \(l > r\) 就一定是矛盾的,直接略过。
再者,如果有 \(cnt\) 个相同的区间 \([l, r]\),那么只能取 \(v = \min(cnt, r - l + 1)\) 个说真话的人,我们可以将每个区域可以获得的最大说真话的人数 \(v\) 记作每个区间的权值。
如果 \([l_1, r_1]\) 和 \([l_2, r_2]\) 有交集,那么两个区间只能取一个,因为 \([l, r]\) 表示的是相同成绩的一个区间,所以我们只要求出不相交集合的权值之和的最大值就可以了。
不相交集合的权值之和的最大值可以使用动态规划完成:
设 \(f_i\) 表示从第一个集合到第 \(i\) 个集合可以获得的最大权值。
\(k\) 表示在 \(1\sim i - 1\) 中找出最大的 \(k\) 满足 \(r_k < l_i\),这个可以用排序 + 二分实现。
最后的答案是 \(n - f_{tot}\),\(tot\) 是集合个数。
代码
代码中有 n, cnt, tot
来表示不同的意义的总量,要注意区分。
/*******************************
| Author: SunnyYuan
| Problem: P2519 [HAOI2011] problem a
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P2519
| When: 2023-11-02 15:40:06
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
struct node {
int l, r, v;
} a[N];
int n;
int f[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
int cnt = 0;
node tmp;
for (int i = 1, x, y; i <= n; i++) {
cin >> x >> y;
tmp = {x + 1, n - y, 0};
if (tmp.l > tmp.r) continue;
a[++cnt] = tmp;
}
sort(a + 1, a + cnt + 1, [](const node a, const node b) { return (a.l == b.l) ? a.r < b.r : a.l < b.l; });
int tot = 0;
for (int i = 1; i <= cnt; i++) {
if (a[i].l == a[tot].l && a[i].r == a[tot].r) a[tot].v++;
else a[++tot] = a[i], a[tot].v = 1;
}
for (int i = 1; i <= tot; i++) a[i].v = min(a[i].v, a[i].r - a[i].l + 1);
sort(a + 1, a + tot + 1, [](const node a, const node b) { return (a.r == b.r) ? a.l < b.l : a.r < b.r; });
// for (int i = 1; i <= tot; i++) cout << a[i].l << ' ' << a[i].r << ' ' << a[i].v << endl;
for (int i = 1; i <= tot; i++) {
int l = 0, r = tot;
while (l < r) {
int mid = l + r + 1 >> 1;
if (a[mid].r < a[i].l) l = mid;
else r = mid - 1;
}
// cout << i << ' ' << l << endl;
f[i] = max(f[i - 1], f[l] + a[i].v);
}
cout << n - f[tot] << '\n';
return 0;
}
UVA529 Addition Chains
题解
首先这道题目不能直接暴搜,因为(可能)会 T,然后我们可以大概得出需要 \(\log n\) 次,所以我们发现这道题目的答案较小,直接 dfs / bfs 都会保存大量的中间无效状态,所以 IDDFS 板子题,直接开写。
等等!我们可以进行优化,第一,假设现在的 \(x\cdot 2^{(dep - cur)} < n\),就不用继续往下搜;第二,可以标记一个 vis
数组,不要搜相同的和(基本没用);第三,可以先搜大的,后搜小的,应该会更快。
代码
/*******************************
| Author: SunnyYuan
| Problem: Addition Chains
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/UVA529
| When: 2023-11-03 19:37:06
|
| Memory: 0 MB
| Time: 3000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, dep;
int ans[N];
bool dfs(int u) {
if (u == dep && ans[u] == n) return true;
if (ans[u] * (1 << (dep - u)) < n) return false;// 优化 1
for (int i = u; i >= 1; i--) { // 优化 2
for (int j = i; j >= 1; j--) {
int sum = ans[i] + ans[j];
if (sum > ans[u] && sum <= n) {
ans[u + 1] = sum;
if(dfs(u + 1)) return true;
}
}
}
return false;
}
void solve() {
for (dep = 1; ; dep++) {
ans[1] = 1;
if (dfs(1)) {
for (int j = 1; j <= dep; j++) cout << ans[j] << " \n"[j == dep];
break;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n, n) solve();
return 0;
}
CF1895D. XOR Construction
这个题目赛场上交了 2 发,在赛后又交了 5 发才过,欢迎爆踩。
思路
看了一眼题解区,为啥都用了 Trie,(直接)贪心不香吗?
首先,我们都不难想到:
把左右两边都异或起来可以推出:\(b_1 \oplus b_{j + 1} = \bigoplus\limits_{i = 1}^{j}a_i\)。
记前缀异或和 \(sum_x = \bigoplus\limits_{i = 1}^{x}a_i\)。
那么 \(b_{j + 1} = b_1 \oplus sum_{j}\),那么实际上是只要确定了 \(b_1\) 就可以算出所有的 \(b_j\)。
题目中要求要让 \(b\) 在 \(0\sim n - 1\) 之内,这实际上实在寻找一个 \(b_1\) 使得异或出来的所有值越小越好,所以我们拆位,假设所有数字的第 \(i\) 位为 \(1\) 的个数大于为 \(0\) 的个数,那我们最好异或上一个 \(2^i\),这样可以使大部分数字变小。
代码
/*******************************
| Author: SunnyYuan
| Problem: D. XOR Construction
| OJ: Codeforces - Educational Codeforces Round 157 (Rated for Div. 2)
| URL: https://codeforces.com/contest/1895/problem/D
| When: 2023-11-04 09:06:50
|
| Memory: 512 MB
| Time: 2000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for (int i = 1; i < n; i++) {
cin >> a[i];
if (i) a[i] ^= a[i - 1];
}
a[0] = 0;
int ans = 0;
for (int i = 0; i < 31; i++) {
int sum1 = 0, sum2 = 0;
for (int j = 0; j < n; j++) {
if (a[j] >> i & 1) sum1++;
else sum2++;
}
if (sum1 > sum2) ans |= 1 << i;
}
for (int i = 0; i < n; i++) a[i] ^= ans;
for (int i = 0; i < n; i++) cout << a[i] << ' ';
return 0;
}
为什么一定可以到 \(0\sim n - 1\),问的人太多了,我截取了我回答的照片:
CF1895E Infinite Card Game
思路
首先我们初始化出每个人出牌后对手出的牌,因为我们要保证消灭对手的同时最大化自己的防御能力,所以第一件事情就是使用排序 + 二分 + 后缀最大值求出对于每一张牌 \((x, y)\) 的对方出的 \((t, v)\) 使得 \(t > x\) 且让 \(v\) 最大化,找出后我们将它们连边,即 \((x, y) \rightarrow (t, v)\)。
然后我们进行拓扑排序,如果拓扑排序后还有剩余的点,那么这些点会形成环,这些点就会造成平手。
然后对于剩下的部分,我们设 \(f_i\) 表示第一次就出牌 \(i\) 谁会赢,如果 \(f_i = 1\) 那么表示先手 Monocarp 赢,\(f_i = 2\) 后手 Bicarp 赢,\(f_i = 0\) 表示此点处于一个环中,上面已经讨论过。
我们从后往前遍历拓扑序,对于当前点 \(u\),如果存在边 \((u, v)\),就将 \(v\) 的状态继承过来,即 \(f_u = f_v\),如果 \(u\) 的出度为 \(0\),那么判断 \(u\) 与 \(n\) 的关系,如果 \(u \le n\),那么 \(f_u = 1\),否则 \(f_u = 2\)。
最后遍历整个 \(f\) 数组求助 3 个答案。
代码
注意不能用 memset
初始化,会 TLE。
/*******************************
| Author: SunnyYuan
| Problem: Infinite Card Game
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/CF1895E
| When: 2023-11-04 15:13:20
|
| Memory: 500 MB
| Time: 3000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 600010, INF = 0x3f3f3f3f;
struct edge {
int to, next;
} e[N];
int head[N], in[N], idx;
void add(int u, int v) {
idx++, e[idx].to = v, e[idx].next = head[u], head[u] = idx, in[v]++;
}
struct node {
int x, y;
bool operator<(const node b) const { return x < b.x; }
} p1[N], p2[N];
int n, m;
int max1[N], max2[N];
int f[N];
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> p1[i].x;
for (int i = 1; i <= n; i++) cin >> p1[i].y;
cin >> m;
for (int i = 1; i <= m; i++) cin >> p2[i].x;
for (int i = 1; i <= m; i++) cin >> p2[i].y;
// 初始化
for (int i = 0; i <= n + m; i++) f[i] = in[i] = 0, head[i] = 0;
max1[n + 1] = max2[m + 1] = idx = 0;
sort(p1 + 1, p1 + n + 1);
sort(p2 + 1, p2 + m + 1);
for (int i = n; i >= 1; i--) {
max1[i] = max1[i + 1];
if (p1[max1[i]].y < p1[i].y) max1[i] = i;
}
for (int i = m; i >= 1; i--) {
max2[i] = max2[i + 1];
if (p2[max2[i]].y < p2[i].y) max2[i] = i;
}
for (int i = 1; i <= n; i++) {
int l = 1, r = m + 1;
while (l < r) {
int mid = l + r >> 1;
if (p2[mid].x > p1[i].y) r = mid;
else l = mid + 1;
}
if (l > m) continue;
add(i, max2[l] + n);
}
for (int i = 1; i <= m; i++) {
int l = 1, r = n + 1;
while (l < r) {
int mid = l + r >> 1;
if (p1[mid].x > p2[i].y) r = mid;
else l = mid + 1;
}
if (l > n) continue;
add(i + n, max1[l]);
}
vector<int> ans;
queue<int> q;
for (int i = 1; i <= n + m; i++) if (!in[i]) q.push(i);
while (q.size()) {
int t = q.front();
q.pop();
ans.push_back(t);
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (!(--in[to])) q.push(to);
}
}
int ans1 = 0, ans2 = 0, ans3 = 0;
for (int j = ans.size() - 1; j >= 0; j--) {
int cur = ans[j];
for (int i = head[cur]; i; i = e[i].next) {
int to = e[i].to;
f[cur] = f[to];
}
if (!head[cur]) {
if (cur > n) f[cur] = 2;
else f[cur] = 1;
}
}
for (int i = 1; i <= n; i++) { // 注意这里是 n 而不是 n + m
if (f[i] == 1) ans1++;
if (f[i] == 0) ans2++;
if (f[i] == 2) ans3++;
}
cout << ans1 << ' ' << ans2 << ' ' << ans3 << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
P2151 [SDOI2009] HH去散步
好久沒有記錄生活了,所以,開記!
思路
開始的時候看錯題了,沒有看到不能走回頭路,所以直接把點連接在了一起,然後矩陣快速冪,然後竟然可以拿到 10 分。
但是如果只考慮點,不好連邊,因爲你不好保證不會走回頭路,所以我們點邊互換!
我們將所有的邊變成點,如果相鄰的兩條邊不是相同的且不是反邊,我們就將這兩條邊的編號連接起來,走 \(t\) 步就相當於在邊上走 \(t - 1\) 步,所以最後的矩陣快速冪要將 \(t - 1\)。
最後 \(O(n^2)\) 枚舉出來兩條邊(現在已經變成點了)\(i, j\),且邊 \(i\) 起點是 \(u\), 邊 \(j\) 的重點是 \(t\),將這個答案 \(g_{i, j}\) 加到答案中。
代碼
注意對反邊的處理,如果使用異或處理,一定要從 \(0\) 或者 \(2\) 開始。
/*******************************
| Author: SunnyYuan
| Problem: P2151 [SDOI2009] HH去散步
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/P2151
| When: 2023-11-08 10:41:57
|
| Memory: 125 MB
| Time: 1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
const int N = 55, M = 130, mod = 45989;
struct road {
int u, v;
} e[M];
struct graph {
graph() { memset(a, 0, sizeof(a)); }
int a[M][M];
} g;
int n, m, t, a, b;
graph operator*(graph a, graph b) {
graph res;
for (int i = 0; i < (m << 1); i++) {
for (int j = 0; j < (m << 1); j++) {
for (int k = 0; k < (m << 1); k++) {
(res.a[i][j] += a.a[i][k] * b.a[k][j]) %= mod;
}
}
}
return res;
}
graph pow(graph a, int b) {
graph res;
for (int i = 0; i < (m << 1); i++) res.a[i][i] = 1;
while (b) {
if (b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> t >> a >> b;
a++, b++;
for (int i = 0, u, v; i < (m << 1); i += 2) {
cin >> u >> v;
u++, v++;
e[i] = {u, v}, e[i + 1] = {v, u};
}
for (int i = 0; i < (m << 1); i++) {
for (int j = 0; j < (m << 1); j++) {
if (i != j && i != (j ^ 1) && e[i].v == e[j].u) {
g.a[i][j] = 1;
}
}
}
g = pow(g, t - 1);
int ans = 0;
for (int i = 0; i < (m << 1); i++) {
for (int j = 0; j < (m << 1); j++) {
if (e[i].u == a && e[j].v == b) {
(ans += g.a[i][j]) %= mod;
}
}
}
cout << ans << '\n';
return 0;
}
CF978F Mentors
思路
这道题目非常简单,首先我们使用二分,每次求出所有 \(a\) 数组中比 \(a_i\) 小的数字个数,记为 \(ans_i\),然后我们处理口角,我们只要贪心,每次将能力较大的那个人的 \(ans - 1\) 即可,因为我们总不能因为一个学员不听话就不欢迎其他学生。
代码
/*******************************
| Author: SunnyYuan
| Problem: Mentors
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/CF978F
| When: 2023-11-08 11:29:08
|
| Memory: 250 MB
| Time: 3000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
int n, k;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
vector<int> ans(n, 0), b(n), a(n);
for (int i = 0; i < n; i++) cin >> a[i], b[i] = a[i];
sort(a.begin(), a.end());
for (int i = 0; i < n; i++) {
int l = -1, r = n - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (a[mid] < b[i]) l = mid;
else r = mid - 1;
}
ans[i] = l + 1;
// cout << ans[i] << ' ';
}
cout << endl;
while (k--) {
int u, v;
cin >> u >> v;
u--, v--;
if (b[u] < b[v]) ans[v]--;
else if (b[v] < b[u]) ans[u]--;
}
for (auto x : ans) cout << x << ' ';
return 0;
}
AcWing 3167 / UVA10228 A Star not a Tree
思路
本题可以使用三分套三分,但是,模拟退火似乎更简单。
很板,不多说了,不会的可以参考我的博客。
代码
注意:
- acwing 上只有一个测试数据;
- UVA 除了最后一个测试点都要输出两个换行,最后一个测试点要一个换行,否则 WA。
/*******************************
| Author: SunnyYuan
| Problem: A Star not a Tree?
| OJ: Luogu
| URL: https://www.luogu.com.cn/problem/UVA10228
| When: 2023-11-08 16:08:40
|
| Time: 3000 ms
*******************************/
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
using PDD = pair<double, double>;
const int N = 110;
int n;
PDD p[N];
double ans;
double dis(const PDD a, const PDD b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
double proc(const PDD s) {
double res = 0;
for (int i = 1; i <= n; i++) res += dis(p[i], s);
ans = min(ans, res);
return res;
}
double rnd(double l, double r) {
return l + 1.0 * (r - l) * rand() / (double)RAND_MAX;
}
void simu() {
PDD u(rnd(0, 10000), rnd(0, 10000));
for (double t = 10000; t > 1e-4; t *= 0.97) {
PDD v(rnd(u.x - t, u.x + t), rnd(u.y - t, u.y + t));
double delta = proc(v) - proc(u);
if (exp(-delta / t) > rnd(0, 1)) u = v;
}
}
void solve(bool endline) {
ans = 1e100;
cin >> n;
for (int i = 1; i <= n; i++) cin >> p[i].x >> p[i].y;
for (int i = 1; i <= 1000; i++) simu();
cout << ans << '\n';
if (endline) cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout << setprecision(0) << fixed;
int T;
cin >> T;
for (int i = 1; i <= T; i++) solve(i != T);
return 0;
}
P2704 炮兵阵地
非常经典的一道题目。
思路
设 \(f_{i, j, k}\) 表示第 \(i\) 行的状态为 \(j\),第 \(i - 2\) 行的状态为 \(k\),那么有:
\(cnt(x)\) 表示数字 \(x\) 在二进制表示下的 \(1\) 的个数。
同时我们还要判断是否满足题目条件。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100, M = 1024;
int n, m;
int g[N];
int f[N][M][M];
int getcnt(int x) {
int cnt = 0;
for (int j = 31; j >= 0; j--) if (x >> j & 1) cnt++;
return cnt;
}
bool check(int x) {
return ((x & (x << 1)) == 0) && ((x & (x << 2)) == 0);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
char c;
cin >> c;
if (c == 'P') g[i] |= (1 << j);
}
}
int ans = 0;
for (int j = 0; j < (1 << m); j++) {
if (check(j) && ((j & g[0]) == j)) ans = max(ans, f[0][j][0] = getcnt(j));
}
for (int i = 1; i < n; i++) {
for (int j = 0; j < (1 << m); j++) {
if (!check(j)) continue;
for (int k = 0; k < (1 << m); k++) {
if (!check(k)) continue;
for (int p = 0; p < (1 << m); p++) {
if ((!check(p)) || (j & k) || (j & p) || ((j & g[i]) != j)) continue;
ans = max(ans, f[i][j][k] = max(f[i][j][k], f[i - 1][k][p] + getcnt(j)));
}
}
}
}
cout << ans << '\n';
return 0;
}
P2167 [SDOI2009] Bill的挑战
很有意思的一道题目。
思路
状态:\(dp_{i, j}\) 表示从左往右前 \(i\) 位选取的字符串为二进制下的 \(j\) 时的合法方案数。
答案:\(\sum dp_{len(s), j}\)
辅助状态:\(f_{i, j}\) 表示下标为 \(i\) 的字符为 \(j\) 的字符串的集合。
状态转移方程:\(dp_{i + 1, j \text{ and } f_{i, c}} = \sum dp_{i, j}\),\(\text{and}\) 表示逻辑与。
初始状态:\(dp_{0, 0} = 1\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 16, M = 55, K = 1 << 15, C = 128, mod = 1000003;
char s[N][M];
int f[M][C], dp[M][K];
int n, k, len;
int getcnt(int x) {
int cnt = 0;
for (int i = 31; i >= 0; i--) {
if (x >> i & 1) cnt++;
}
return cnt;
}
void solve() {
memset(dp, 0, sizeof(dp));
memset(f, 0, sizeof(f));
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> s[i];
len = strlen(s[0]);
for (int i = 0; i < len; i++) {
for (char j = 'a'; j <= 'z'; j++) {
for (int k = 0; k < n; k++) {
if (s[k][i] == j || s[k][i] == '?') f[i][j] |= (1 << k);
}
}
}
for (char c = 'a'; c <= 'z'; c++) dp[0][f[0][c]]++;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < (1 << n); j++) {
for (char c = 'a'; c <= 'z'; c++) {
(dp[i + 1][j & f[i + 1][c]] += dp[i][j]) %= mod;
}
}
}
int ans = 0;
for (int i = 0; i < (1 << n); i++) {
if (getcnt(i) == k) {
(ans += dp[len - 1][i]) %= mod;
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
P3694 邦邦的大合唱站队
思路
设 \(f_i\) 为已经排完的人的集合在二进制表示下为 \(i\),那么有
\(L, R\) 是可以推算出来的。
代码
本来以为从 \(0\) 开始肯定好写,结果细节比较多(?)还是这样写了。
#include <bits/stdc++.h>
using namespace std;
const int N = 20, M = 100010;
int f[1 << N];
int n, m, a[M];
int sum[M][N];
int gettot(int x) {
int tot = -1;
for (int j = 0; j < m; j++) {
if (x >> j & 1) tot += sum[n - 1][j];
}
return tot;
}
int getsum(int l, int r, int x) {
if (l) return r - l + 1 - (sum[r][x] - sum[l - 1][x]);
else return r - l + 1 - (sum[r][x]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 0; i < n; i++) {
if (i) memcpy(sum[i], sum[i - 1], sizeof(sum[i]));
cin >> a[i];
a[i]--;
sum[i][a[i]]++;
}
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for (int i = 1; i < (1 << m); i++) {
for (int j = 0; j < m; j++) {
if (i >> j & 1) {
int l = gettot(i - (1 << j)) + 1;
int r = l + sum[n - 1][j] - 1;
f[i] = min(f[i], f[i - (1 << j)] + getsum(l, r, j));
}
}
}
cout << f[(1 << m) - 1] << '\n';
return 0;
}
P1516 青蛙的约会
思路
实际上是要解 \((x + m \cdot cnt) \bmod L = (y + n \cdot cnt) \bmod L\)。
转化:
\(x - y, m - n\) 已知,可求 \(cnt, k\)。
当 \(m - n\) 是个负数,将 \(x, y\) 和 \(m, n\) 分别互换。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
i64 x, y, m, n, l;
i64 exgcd(i64 a, i64 b, i64 &x, i64 &y) {
if (!b) {
x = 1, y = 0;
return a;
}
i64 g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> x >> y >> m >> n >> l;
if (m < n) swap(m, n), swap(x, y);
// int a = m - n , b = L, c = y - x;
i64 ans_a = 0, ans_b = 0;
i64 g = exgcd(m - n, l, ans_a, ans_b);
if ((y - x) % g) {
cout << "Impossible\n";
return 0;
}
ans_a *= (y - x) / g;
((ans_a %= l / g) += l / g) %= l / g;
cout << ans_a << '\n';
return 0;
}