2022 年 9 月水题选做
20220901
SP30919 GCDS - Sabbir and gcd problem
思路:显然答案就是不是任意一个数的因数的最小的质数。这个可以在线性筛的时候记录每个数的最小的素因数即可。
算法:线性筛。
技巧:线性筛可以筛每个数的最小的素因数。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1e5, W = 1e7;
int n, a[N + 10];
int prm[W + 10], notPrm[W + 10], totp, b[W + 10], notOk[W + 10];
void sieve() {
for (int i = 2; i <= W; i++) {
if (!notPrm[i]) prm[++totp] = i, b[i] = i;
for (int j = 1; j <= totp && i * prm[j] <= W; j++) {
notPrm[i * prm[j]] = 1;
b[i * prm[j]] = prm[j];
if (i % prm[j] == 0) break;
}
}
}
void mian() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
for (int j = a[i]; j > 1; j /= b[j]) notOk[b[j]] = 1;
}
for (int i = 1; i <= totp; i++)
if (!notOk[prm[i]]) return printf("%d\n", prm[i]), void();
}
int main() {
sieve();
int T; scanf("%d", &T);
while (T--) { memset(notOk, 0, sizeof(notOk)); mian(); }
return 0;
}
20220905
CF1061C Multiplicity
思路:设 \(f_{i,j}\) 为考虑前 \(i\) 个数,子序列长度为 \(j\) 的方案数,则:
第一维可以压掉。然后我们发现真正要被转移到的地方很少,于是时间复杂度也 OK 了。
算法:dp。
技巧:只考虑有用状态。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <functional>
#include <vector>
using namespace std;
const int N = 1e5, P = 1e9 + 7;
int n, a[N + 10], f[N + 10];
vector<int> d[N + 10];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
for (int j = 1; j <= int(sqrt(a[i])); j++)
if (a[i] % j == 0) {
d[i].push_back(j);
if (j * j != a[i]) d[i].push_back(a[i] / j);
}
sort(d[i].begin(), d[i].end(), greater<int>());
}
f[0] = 1;
for (int i = 1; i <= n; i++)
for (auto j : d[i])
if (j <= n) (f[j] += f[j - 1]) %= P;
int ans = 0;
for (int i = 1; i <= n; i++)
(ans += f[i]) %= P;
printf("%d\n", ans);
return 0;
}
P2606 [ZJOI2010]排列计数
思路:把不等关系画成一棵树(根为 \(1\))然后 dp。设 \(f_u\) 表示以 \(u\) 为根的子树的答案,则:
注意模数可以很小,所以求组合数要用 Lucas 定理。
算法:树形 dp、计数、Lucas。
技巧:建立不等关系树。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <tuple>
using namespace std;
const int N = 1e6;
int n, P;
int fac[N + 10], ifac[N + 10];
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1LL * res * a % P;
a = 1LL * a * a % P; b >>= 1;
}
return res;
}
void init() {
int lim = min(N, P - 1);
fac[0] = 1;
for (int i = 1; i <= lim; i++)
fac[i] = 1LL * fac[i - 1] * i % P;
ifac[lim] = qpow(fac[lim], P - 2);
for (int i = lim - 1; i >= 0; i--)
ifac[i] = 1LL * ifac[i + 1] * (i + 1) % P;
}
int comb(int a, int b) {
if (a < 0 || b < 0 || a < b) return 0;
return 1LL * fac[a] * ifac[b] % P * ifac[a - b] % P;
}
int C(int a, int b) {
if (a < P && b < P) return comb(a, b);
return 1LL * C(a / P, b / P) * comb(a % P, b % P) % P;
}
pair<int, int> f(int x) {
if (x > n) return {1, 0};
int la, ls, ra, rs;
tie(la, ls) = f(x * 2);
tie(ra, rs) = f(x * 2 + 1);
return {1LL * la * C(ls + rs, ls) % P * ra % P, ls + rs + 1};
}
int main() {
scanf("%d%d", &n, &P);
init();
printf("%d\n", f(1));
return 0;
}
CF245H Queries for Number of Palindromes
思路:先用一遍 dp 求出每个子串是不是回文的,然后求一遍这个 dp 数组的二维前缀和。
算法:dp、前缀和。
技巧:回文串问题可以区间 dp。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 5000;
int n, m;
char s[N + 10];
bool f[N + 10][N + 10];
int g[N + 10][N + 10];
int main() {
scanf("%s", s + 1);
n = int(strlen(s + 1));
for (int i = 1; i <= n; i++) {
f[i][i] = 1;
if (i < n) f[i][i + 1] = s[i] == s[i + 1];
}
for (int len = 3; len <= n; len++)
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
f[l][r] = s[l] == s[r] && f[l + 1][r - 1];
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
g[i][j] = g[i - 1][j] + g[i][j - 1] - g[i - 1][j - 1] + f[i][j];
scanf("%d", &m);
while (m--) {
int l, r; scanf("%d%d", &l, &r);
printf("%d\n", g[r][r] - g[l - 1][r] - g[r][l - 1] + g[l - 1][l - 1]);
}
return 0;
}
20220907
P6006 [USACO20JAN]Farmer John Solves 3SUM G
思路:显然可以预处理答案。考虑枚举 \(i,k\),那么 \((i,k)\) 可以贡献到 \(l\le i,k\le r\) 的询问,\((i,k)\) 的答案即满足 \(i\lt j\lt k,a_j=-(a_i+a_k)\) 的 \(j\) 的个数。
算法:桶、前缀和。
技巧:预处理答案、考虑贡献。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 5000, W = 2e6;
int n, m, a[N + 10], ccnt[W + 10], *cnt = ccnt + W / 2 + 5;
ll g[N + 10][N + 10];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
g[i][j] = -W / 2 <= a[i] + a[j] && a[i] + a[j] <= W / 2 && j - i >= 2 ? cnt[-(a[i] + a[j])] : 0;
cnt[a[j]]++;
}
for (int j = i + 1; j <= n; j++)
cnt[a[j]]--;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
g[i][j] = g[i - 1][j] + g[i][j - 1] - g[i - 1][j - 1] + g[i][j];
while (m--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%lld\n", g[r][r] - g[r][l - 1] - g[l - 1][r] + g[l - 1][l - 1]);
}
return 0;
}
P6007 [USACO20JAN]Springboards G
思路:下面用 \(m\) 代表题目中的 \(n\),\(n\) 代表题目中的 \(p\)。
注意到我们如果不用任何跳板,答案就是 \(2m\),我们可以计算使用跳板能省多少。于是题目就变成了:选择一些跳板,使其满足偏序关系,且能省的距离最大。
显然有一个 \(\mathcal O(n^2)\) dp:对于一个跳板,从它左下角的所有跳板转移过来。考虑优化。
于是这就是一个二维数点。我们可以扫描线维护 \(x\) 轴,树状数组维护 \(y\) 轴;遇到一个起点就更新 dp,遇到一个终点就更新树状数组。
for (int i = 1; i <= 2 * n; i++) {
if (a[i].type == 0) { // 起点
f[a[i].id] = query(a[i].y) + dis[a[i].id];
} else { // 终点
modify(a[i].y, f[a[i].id]);
}
}
算法:dp、二维数点、数据结构优化 dp。
技巧:二维数点(扫描线 + 数据结构维护)。
想到了的:dp、数据结构优化。
没想到的:二维数点具体维护方法。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 2e5;
struct Node {
int x, y, id, type;
};
int n, m;
Node a[N + 10];
int dis[N + 10], f[N + 10];
int tx[N + 10], ty[N + 10];
int c[N + 10];
#define lowbit(x) (x & (-x))
void modify(int p, int x) { for (; p <= 2 * n; p += lowbit(p)) c[p] = max(c[p], x); }
int query(int p) { int res = 0; for (; p; p -= lowbit(p)) res = max(res, c[p]); return res; }
#undef lowbit
int main() {
scanf("%d%d", &m, &n);
for (int i = 1; i <= n; i++) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
a[i * 2 - 1] = {x1, y1, i, 0};
a[i * 2] = {x2, y2, i, 1};
tx[i * 2 - 1] = x1, tx[i * 2] = x2;
ty[i * 2 - 1] = y1, ty[i * 2] = y2;
dis[i] = x2 - x1 + y2 - y1;
}
sort(tx + 1, tx + 2 * n + 1);
sort(ty + 1, ty + 2 * n + 1);
int totx = int(unique(tx + 1, tx + 2 * n + 1) - tx - 1);
int toty = int(unique(ty + 1, ty + 2 * n + 1) - ty - 1);
for (int i = 1; i <= n; i++) {
a[i * 2 - 1].x = lower_bound(tx + 1, tx + totx + 1, a[i * 2 - 1].x) - tx;
a[i * 2 - 1].y = lower_bound(ty + 1, ty + toty + 1, a[i * 2 - 1].y) - ty;
a[i * 2].x = lower_bound(tx + 1, tx + totx + 1, a[i * 2].x) - tx;
a[i * 2].y = lower_bound(ty + 1, ty + toty + 1, a[i * 2].y) - ty;
}
sort(a + 1, a + 2 * n + 1, [](const Node &lhs, const Node &rhs) {
return lhs.x == rhs.x ? lhs.y < rhs.y : lhs.x < rhs.x;
});
for (int i = 1; i <= 2 * n; i++) {
if (a[i].type == 0) {
f[a[i].id] = query(a[i].y) + dis[a[i].id];
} else {
modify(a[i].y, f[a[i].id]);
}
}
printf("%d\n", 2 * m - *max_element(f + 1, f + n + 1));
return 0;
}
CF1139D Steps to One
思路:推式子。
于是我们继续研究 \(P(\gt k)\):
代回原式:
线性筛 \(\mu\) 即可。
算法:期望、莫反。
技巧:莫反套路、\(E(x)=\sum_{i=1}^{+\infty}P(i)\cdot i=\sum_{i=1}^{+\infty}P(\ge i)=1+\sum_{i=1}^{+\infty}P(\gt i)\)。
想到了的:莫反。
没想到的:那个期望的结论。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 1e5, P = 1e9 + 7;
int n, prm[N + 10], notPrm[N + 10], totp, mu[N + 10];
void sieve() {
mu[1] = 1;
for (int i = 2; i <= N; i++) {
if (!notPrm[i]) prm[++totp] = i, mu[i] = -1;
for (int j = 1; i * prm[j] <= N; j++) {
notPrm[i * prm[j]] = 1;
if (i % prm[j] == 0) { mu[i * prm[j]] = 0; break; }
mu[i * prm[j]] = -mu[i];
}
}
}
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1LL * res * a % P;
a = 1LL * a * a % P; b >>= 1;
}
return res;
}
int main() {
sieve();
scanf("%d", &n);
int ans = 0;
for (int i = 2; i <= n; i++) {
int q = n / i;
(ans += 1LL * mu[i] * q % P * qpow(n - q, P - 2) % P) %= P;
}
ans = 1 - ans;
printf("%d\n", (ans % P + P) % P);
}