总结
集合
考虑枚举子集和,统计有多少个子集的和为当前枚举的子集和,然后我们记个结论:\(x^y=x^{y \mod (p - 1)}\),然后就过了
P3488
一眼二分图(网络流启动),但是考虑到图很大,所以我们考虑直接判断是否是二分图,考虑一个区间,如果总数比这个区间所能承载的人都要大,那么肯定会寄,所以用线段树维护每个区间的最大字段和
连通块
考虑没有限制,那么就是一个树上的 \(dp\),加上限制后,我们考虑,每加一个子树相当于加一个dfn序,所以我们可以记录当前dfn最后一个位置,那么如果要加一个子树,那么就可以从限制点的位置-1的地方转移过来,但是我们只要处理限制点,所以我们可以离散化,然后只处理限制点
跳棋
考虑整个序列是如何变动的,我们观察到,当两个 1 组在一起时,是可以一直动的,因为碰到一个 1 时,可以从跳跃换成接替,那么我们便可以将 11 压在一起变成 2,那么数组终将会有四种元素:\(0,1,2,?\)
考虑没有问号的情况,那么整个问题变成了 2 的放置,我们发现 1 没有贡献,所以我们只用看 0 和 2 的个数,那么变成了在 0 和 2 的个数和中选取 2 的个数个位置放置 2,那么我们可以用 \(dp\) 的方式来给 \(?\) 分配符号,考虑到答案是与 0 的个数, 和 2 的个数有关,所以我们记录下 0 的个数和 2 的个数,那么我们初始认为状态时 \(f_{ijk}\) 表示前 \(i\) 个,有 \(j\) 个 2 \(k\) 个 0
但是我们要思考 \(j\) 和 \(k\) 要如何统计,那么统计我们需要知道上一位是什么,但是考虑到 \(2\) 的统计是当前面有偶数个 \(1\) 的时候才能凑齐 2,所以我们要加一个前面有奇数偶数个 1 所以状态变成 \(f_{ijk\ 0/1\ 0/1}\)
考虑转移,那么从当前是什么来考虑,分几种当前是什么的情况,然后注意当只有当前面有偶数个 1 的时候才能统计 \(2\) 的个数
最后对于目标状态,由于已经不存在 ? 所以可以直接用组合数做,然后要滚动数组
总结
由于思路错误,导致大部分时间在想 Dinic 如何解决删人问题,所以其它题基本没有想(不过我Dinic写对了!)。
114
我们手玩的知,选取方式如 \(AABABABAB\) 的形式,而 0 的存在可以将最坏情况交给对面,所以 0 起调整作用,然后在 1 和 2 中选择一个进行判断,如果存在一个数可以使得先手必胜,那么先手必胜
514
考虑第一个 \(a_i < i\) 的位置,假设这个位置是 \(i\)
根据题意我们得知:\(a_1\) 可以填的数种类为 \(n - 1 + 1\),\(a_2\) 可以填的数种类为 \(n - 2 + 1\),于是我们知道 \(i - 1\) 可以填 \(n - i + 2\),但是 \(i\) 已经占掉了一个前面的,所以变成了 \(n - i + 1\),然后同理我们可以得到 \(i - 1\) 可以放 \(n - i + 2\) 种,但是由于 \(i - 1\) 又占了一个,所以也为 \(n - i + 1\) 以此类推我们可以得到前 \(j\) 的方案数:\((n - i + 1) ^ j\),不过我们在实际情况下会存在一个断点,是的前面的是小于等于 \(j\) 的填数,而其他的在后面,那么其他的为 \(n - (i - 1) + 1\) 种数,那么方案数便是 \((n - i + 2) ^ {i - j - 1}\)
整理一下柿子:
\(ans=n+1+\sum\limits_{i=1}^{n}(\sum\limits_{j=1}^{i-1}((n-i+1)^j(n-i+2)^{i-j-1})(n-i)!i)\)
考虑一下换元,设 \(x = n - i + 1\),设 \(y = \frac{x}{x+1}\) 那么可得:
\(ans=n+1+\sum\limits_{i=1}^{n}(\sum\limits_{j=1}^{i-1}((x)^j(x + 1)^{i-j-1})(n-i)!i)\)
\(ans=n+1+\sum\limits_{i=1}^{n}(\sum\limits_{j=1}^{i-1}((x)^j(x + 1)^{i-1}(x+1)^{-j})(n-i)!i)\)
\(ans=n+1+\sum\limits_{i=1}^{n}((x + 1)^{i-1}\sum\limits_{j=1}^{i-1}((x)^j(x+1)^{-j})(n-i)!i)\)
\(ans=n+1+\sum\limits_{i=1}^{n}((x + 1)^{i-1}\sum\limits_{j=1}^{i-1}(\frac{x}{x+1})^{j}(n-i)!i)\)
\(ans=n+1+\sum\limits_{i=1}^{n}((x + 1)^{i-1}\sum\limits_{j=1}^{i-1}y^{j}(n-i)!i)\)
\(ans=n+1+\sum\limits_{i=1}^{n}((x + 1)^{i-1}\sum\limits_{j=1}^{i-1}\frac{y^2-y}{y-1}(n-i)!i)\)
然后就可以 \(O(n)\) 解决了
1919
考虑将一个区间查询变成前缀异或和,那么设 \(S(x)\) 为 \(1\) 到 \(x\) 的答案,考虑每次消掉最大的fib,我们每次删掉最大的fib,然后分为两段进行计算,然后计算被消掉的数量,如果为奇数则异或后不为 0 所以异或即可。
810
完全背包的优化,如果有连续的 \(min\) 个的可以,那么接下来的肯定都能被表示,所以直接剪枝
总结
数学场我是一点都写不了啊!
智乃的差分
构造。What can I say
牛牛的旅行
一眼淀粉质,但是可以将贡献拆成点带来的和边带来的,那么可以对每条边求出经过次数,对于每个点,从小到大排序,然后将以这个为路径上最大的点对计算贡献,具体来说就是将并查集合并在合并的过程中,计算两个并查集经过最大点的点对数
第K排列
用 dp 优化搜索,我们可以想到一个暴力,就是暴力枚举填什么,然后最后再去判,这个的依据是 k 很小,不过我们发现可能判出很多没必要的,所以我们可以计算一下从后往前的最大填法,如果用最大填法都无法达到目标就没有必要继续填了,所以我们先维护一个从后往前的最大填法 dp,然后暴力从前往后填,好像是叫 A* 吧。
牛牛的 border
考虑枚举子串作为 border,假设这个出现了 cnt 次,长度为 len,那么以这个为 border 的字串将会有 \(\frac{cnt * (cnt - 1)}{2}\) 个(组合数),然后考虑到是 border,那么我们可以从中删掉一些字符,使得还可以作为 border,所以还要计算删掉一些字符,我们考虑用 SAM 这样就确定了左端点,所以我们就只用删掉后缀,那么便可以用等差数列计算了即:\(\frac{len * (len + 1)}{2}\)(因为对于 1 ~ len 的长度都会出现在删掉后缀的 border 中),然后如果要用 SA 就需要处理一些重复的位置,我的评价是,SAM 这个数据结构,甚至说 trie 树都直接将这个多余的一部给剩了(重复的将会合并在条路径中)
#include <iostream>
using namespace std;
using ll = long long;
const int MaxN = 1e5 + 10;
namespace SAM {
ll nxt[MaxN << 1][27], fail[MaxN << 1], len[MaxN << 1], cnt[MaxN << 1], t[MaxN << 1], p[MaxN << 1], tot;
void copy(int x, int y) {
for (int i = 0; i < 27; i++) nxt[x][i] = nxt[y][i];
fail[x] = fail[y];
}
void insert(string s) {
tot = 1;
for (int i = 0, c, to, p, lstp = 1; i < s.size(); i++, nxt[lstp][c] = to, lstp = to) {
c = s[i] - 'a', len[to = ++tot] = len[p = lstp] + 1, cnt[to] = 1;
for (p; p && !nxt[p][c]; p = fail[p]) {
nxt[p][c] = to;
}
if (!p && (fail[to] = 1)) continue;
int v = nxt[p][c], cl = ++tot;
if (len[v] == len[p] + 1 && (tot--, fail[to] = v)) continue;
for (copy(cl, v), len[cl] = len[p] + 1; p && nxt[p][c] == v; p = fail[p]) {
nxt[p][c] = cl;
}
fail[to] = fail[v] = cl;
}
for (int i = 1; i <= tot; i++) t[len[i]]++;
for (int i = 1; i <= tot; i++) t[i] += t[i - 1];
for (int i = 1; i <= tot; i++) p[t[len[i]]--] = i;
for (int i = tot; i >= 1; i--) cnt[fail[p[i]]] += cnt[p[i]];
}
} // namespace SAM
using namespace SAM;
string s;
ll ans;
int n;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
freopen("border.in", "r", stdin);
freopen("border.out", "w", stdout);
cin >> n >> s;
insert(s);
for (int i = 1; i <= tot; i++) {
ans += cnt[p[i]] * (cnt[p[i]] - 1) / 2 * (len[p[i]] + len[fail[p[i]]] + 1) * (len[p[i]] - (len[fail[p[i]]] + 1) + 1) / 2;
}
cout << ans << endl;
return 0;
}
总结
死磕 B 题,然后写了个淀粉质常数巨大后 T 爆,A 题想到了可还是有情况没想全,最后爆 0
你相信()吗
考场上推了三个小时,然后差一步,最后 30 呜呜呜。
考虑推个柿子:
\(a + \frac{b + c}{2} + \frac{d}{4} \ge A\)
\(d + \frac{b + c}{2} + \frac{a}{4} \ge D\)
\(b + \frac{a + d}{2} + \frac{c}{4} \ge B\)
\(c + \frac{a + d}{2} + \frac{b}{4} \ge C\)
我们设 \(a = 4k+d\),然后进行推柿子,于是我们可以通过关于 \(BC\) 的柿子和 \(k\) 的范围来判断可行性,然后就用二分暴力算即可
奇怪的函数
哇,这道题没做出来简直唐诗(其实我没怎么看题)。
一眼线段树,于是考虑信息与标记,我们会发现可以将最终的函数看作一个取值范围的区间,但是考虑到有加减操作,于是我们单纯维护取值范围,并用一个标记维护加减操作
- 信息与信息
如果信息维护的区间有相交,那么合并后的区间将会是相交部分吗,如果没有的话,那么肯定是左边的所有可能值到右边后全变小或全变大,所以区间将会是变小后的值或是变大后的值
- 信息与标记
考虑到信息维护的区间是不加加减操作的取值范围,所以在采用加减操作时只要把左右两端都加上加减操作即可。
- 标记与标记
直接相加,因为都是加减操作
#include <iostream>
using namespace std;
const int MaxN = 3e5 + 10, inf = 1e9;
struct S {
int l, r, w;
S operator+(const S &j) const {
if (r < j.l - w) return {j.l - w, j.l - w, w + j.w};
if (l > j.r - w) return {j.r - w, j.r - w, w + j.w};
return {max(j.l - w, l), min(j.r - w, r), w + j.w};
}
} d[MaxN << 2];
int n, m, op, p, x;
void update(int k, int op, int x, int l = 1, int r = n, int p = 1) {
if (l == r) {
if (op == 1) d[p] = {-inf, inf, x};
if (op == 2) d[p] = {-inf, x, 0};
if (op == 3) d[p] = {x, inf, 0};
return;
}
int mid = l + r >> 1;
if (k <= mid) update(k, op, x, l, mid, p << 1);
if (k > mid) update(k, op, x, mid + 1, r, p << 1 | 1);
d[p] = d[p << 1] + d[p << 1 | 1];
}
int main() {
freopen("function.in", "r", stdin);
freopen("function.out", "w", stdout);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> op >> x, update(i, op, x);
}
for (cin >> m; m; m--) {
cin >> op;
if (op == 4) {
cin >> x;
cout << (x < d[1].l ? d[1].l + d[1].w : x > d[1].r ? d[1].r + d[1].w : x + d[1].w) << endl;
} else {
cin >> p >> x;
update(p, op, x);
}
}
return 0;
}
博弈
淀粉质板子,但是不会哈希技巧而死!
没啥好说的,就是可以给边权附一个随机的数值,然后哈希即可
强烈谴责 -+ 不断取消我的成绩
#include <ctime>
#include <iostream>
#include <unordered_map>
#include <random>
#include <vector>
using namespace std;
using ll = long long;
const int MaxN = 5e5 + 10;
struct S {
ll v, w;
};
ll sz[MaxN], tmpp[MaxN], tot, n, k, ans, SUM, t;
vector<S> g[MaxN];
bool vis[MaxN];
unordered_map<ll, ll> st, cnt;
int find_fatbigest(int x, int fa) {
sz[x] = 1;
ll maxs = 0, res = -1;
for (auto i : g[x]) {
if (i.v == fa || vis[i.v]) continue;
res = find_fatbigest(i.v, x);
if (res != -1) {
return res;
}
sz[x] += sz[i.v], maxs = max(maxs, sz[i.v]);
}
maxs = max(maxs, n - sz[x]);
if (maxs * 2 <= n) {
res = x;
sz[fa] = n - sz[x];
}
return res;
}
void G(int x, int fa, ll sum) {
tmpp[++tot] = sum;
ans += SUM - cnt[sum] + bool(sum);
for (auto i : g[x]) {
if (i.v == fa || vis[i.v]) continue;
G(i.v, x, sum ^ i.w);
}
}
void DFS(int x) {
for (auto i : g[x]) {
if (vis[i.v]) continue;
int tmp = tot;
G(i.v, x, i.w);
for (int i = tmp + 1; i <= tot; i++) {
cnt[tmpp[i]]++;
SUM++;
}
}
SUM = 0;
unordered_map<ll, ll>().swap(cnt);
tot = 0;
vis[x] = 1;
for (auto i : g[x]) {
if (vis[i.v]) continue;
n = sz[i.v];
DFS(find_fatbigest(i.v, x));
}
}
int main() {
freopen("game.in", "r", stdin);
freopen("game.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
mt19937_64 rnd(time(0));
for (cin >> t; t; t--) {
cin >> n, ans = 0;
for (int i = 1; i <= n; i++) {
vector<S>().swap(g[i]), vis[i] = 0;
}
unordered_map<ll, ll>().swap(st);
for (int i = 1, u, v, w; i < n; i++) {
cin >> u >> v >> w;
if (!st.count(w)) st[w] = uniform_int_distribution<ll>(1, 1e18)(rnd);
g[u].push_back({v, st[w]});
g[v].push_back({u, st[w]});
}
DFS(find_fatbigest(1, 0));
cout << ans << '\n';
}
return 0;
}
跳跃
考虑贪心,我们肯定是在一个最大子段和中跳来跳去,那么我们需要思考什么时候第一时间到一个点和此时的最大和,然后就可以暴力dp
大陆
我们对于大于等于 B 的分一块,然后将小于 B 的传上去,如果根这一连通块大小小于 B ,我们可以将其加给任意一个,因为任意一个都肯定小于 2B,加一个后也小于等于 3B
排列
我是智障,循环右移原来是一段区间的移动
考虑线段树,然后发现无法移动,所以转成 FHQ,但是 FHQ 需要用下标作为分裂合并
考虑如何维护答案,我们可以先求二元的,那么将会是一个最大最小值,那么三元的我们可以维护小于最大值的最右值,和大于最小值的最左值,然后进行一个拼凑,在 FHQ 上搜索即可
#include <algorithm>
#include <ctime>
#include <random>
#include <iostream>
using namespace std;
mt19937 myrand(time(0));
uniform_int_distribution<int> randpp(1, 1e9);
const int MaxN = 2e5 + 10;
const int inf = 1e9;
struct Tree {
struct Node {
int minx, miny, maxx, maxy, w, x, v, l, r, sum;
} a[MaxN];
int root, tot;
Tree() {
root = 0;
a[root].minx = a[root].miny = inf;
a[root].maxx = a[root].maxy = -inf;
}
int pre(int x, int l) {
if (!x) return -inf;
if (a[x].x < l) return max(pre(a[x].l, l), a[x].x);
return pre(a[x].r, l);
}
int nxt(int x, int r) {
if (!x) return inf;
if (a[x].x > r) return min(nxt(a[x].r, r), a[x].x);
return nxt(a[x].l, r);
}
void update(int k, int ls = 0, int rs = 0) {
ls = a[k].l, rs = a[k].r;
a[k].sum = a[ls].sum + a[rs].sum + 1;
a[k].w = a[ls].w | a[rs].w;
if (min(a[k].x, a[ls].minx) < a[rs].maxy || a[ls].miny < max(a[k].x, a[rs].maxx) || (a[ls].minx < a[k].x && a[k].x < a[rs].maxx)) a[k].w = 1;
a[k].minx = min({a[k].x, a[ls].minx, a[rs].minx}), a[k].maxx = max({a[k].x, a[ls].maxx, a[rs].maxx});
a[k].miny = min(a[ls].miny, a[rs].miny), a[k].maxy = max(a[ls].maxy, a[rs].maxy);
(a[k].x > a[ls].minx) && (a[k].miny = min(a[k].miny, a[k].x));
(a[k].x < a[rs].maxx) && (a[k].maxy = max(a[k].maxy, a[k].x));
a[k].miny = min(a[k].miny, nxt(rs, min(a[ls].minx, a[k].x)));
a[k].maxy = max(a[k].maxy, pre(ls, max(a[rs].maxx, a[k].x)));
}
void split(int k, int v, int &x, int &y) {
if (!k) {
x = y = 0;
return;
}
if (a[a[k].l].sum < v) {
x = k;
split(a[k].r, v - a[a[k].l].sum - 1, a[k].r, y);
} else {
y = k;
split(a[k].l, v, x, a[k].l);
}
update(k);
}
void merge(int &k, int x, int y) {
if (!x || !y) {
k = x + y;
return;
}
if (a[x].v > a[y].v) {
merge(a[x].r, a[x].r, y);
k = x;
} else {
merge(a[y].l, x, a[y].l);
k = y;
}
update(k);
}
void insert(int id, int v) {
a[++tot] = {v, inf, v, -inf, 0, v, int(randpp(myrand) % inf), 0, 0, 1};
merge(root, root, tot);
}
} tr;
int n, m, l, r, k;
int main() {
freopen("permutation.in", "r", stdin);
freopen("permutation.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1, w; i <= n; i++) {
cin >> w;
tr.insert(i, w);
}
for (cin >> m; m; m--) {
cin >> l >> r >> k;
int A = 0, B = 0, C = 0, D = 0, E = 0, F = 0, G = 0;
tr.split(tr.root, r, F, G);
tr.split(F, r - k, D, E);
tr.split(D, l - 1, B, C);
tr.merge(B, B, E);
tr.merge(C, C, G);
tr.merge(tr.root, B, C);
cout << (tr.a[tr.root].w ? "YES" : "NO") << '\n';
}
return 0;
}
躲避技能
我们考虑一个贪心的想法,我们让可以不出子树的地方尽量不出子树,那么出子树的情况当且仅当位置不够,所以我们维护一下点数和坑数的差就行了
帮助
考虑将其在图中表示,然后就可以获得一个扫描线板子
跑步路线
我靠!没看到边权不同!!!!!!以为最小生成树不唯一!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
fountain
我们可以使用一个高级的设元,然后就过了
subset
这个题分为两个模块,第一个是当函数为 0 时的计算,另一部分为不断删掉集合中的一些数,然后进行计算,
A
我们可以将删掉立方体改成一个二维的矩形体现在数轴上,然后我们将每一面的二维矩阵进行处理,会发现是对于每一个 x 的最大的 y 然后删掉,那么我们对于每一个面进行延申到其他面,我们会发现,当一个横截面移动时,三条线的移动都是单调的,那么我们便可以通过横截面的移动,用预处理出来的三条线确定我们删掉的体积。
B
考虑将贡献转化,转换为每一个位置的答案会对总答案产生多少贡献,那么我们枚举哪一个位置将会留下,设计状态 \(f_{id, i, j}\) 表示 id 留下,i 次删,当前留下的数的排名,那么每次我们有两种转移,一种删前面的一种删后面的,删前面的时候我们定义删的是 \(k\),那么会有 \((1-p_i)^{k-1}*p_i\) 的概率成功,那么对于所有的排名小于当前的我们可以统一计算概率,整理一下得:\(1 - qpow(1 - p[j - 1], k)\) 的概率,而在后面的也同理易得
C
分治板子,我们考虑每次删掉一个不合法的,将区间分为两半,然后就可以求出来最大的 k 了,这样的时间复杂度我们发现,每次划分当且仅当出现了一种新的出现次数,那么如果要求出现次数都不同,那么分布将会是 \(\sqrt(n)\) 级别的,所以时间复杂度是 \(O(n\sqrt(n))\) 级别的。
考虑如何优化,我们用启发式合并,只动小的,那么启发式合并带给我们的将会是总和 \(nlogn\) 的时间复杂度
遇到区间,不带单调性,还带明显特征的分段点的题,分治启动(虽然后面寄了,因为是单峰)
砖块摆放
手玩,发现规律:\(z=3-x-y\) 二项式秒了
植物收集
嗨嗨嗨!现场推三分过了!
我们想一个错解,我们每次只给一个施肥,那么我们可以用dij解决,但是我们发现,我们一次给的是一堆施肥,所以我们的施肥费用其实之和最大是施肥数有关,那么我们可以将贡献非为两类,一个是最大施肥次数,一个是种子购买数,我们枚举最大施肥次数,然后对于每一个进行判断,我们发现将是在一个区间上求最小值,然后我们观察可得,这个区间最小值将会是一个下凸壳,而最大施肥次数将会是一个线性函数,两个加在一起将会是一个单峰函数,然后三分启动