最近一段时间的做题记录
前言
不知不觉已快到九月,最近半个月好像一直在联考,几乎没有时间留给我整理做过的题,趁着有空赶快写一些,不想放过好题鸭qwq。
随机树
第一问设 \(f_i\) 表示有 i 个叶子的数其叶子平均深度。转移考虑选一个叶子拓展会将总深度增大 \(f_{i-1}+2\),于是有:
整理可得:\(f_i=f_{i-1}+{2\over x}\)。
第二问挺神秘的,但是不出意外我们还是用 dp 做。这时只设一个状态肯定不够,考虑增加什么?注意到深度期望我们可以由定义转化为深度乘对应概率,于是我们多记录深度,dp 值就是概率。但是这样也太难转移了吧!于是放宽限制,考虑深度小于一个值的概率。但是如果你去自己写一下转移发现这个转移到大于某个深度是简单的于是再改一下,最终状态是 \(f_{x,i}\) 表示有 x 个叶子的树深度大于等于 i 的概率。
转移考虑枚举树的左右部分,于是有:
其中 \(P_k\) 表示生成一棵左边 k 个叶子的树满足深度大于等于 j 的概率。我们先假设操作 i-1 次的概率为 \(P'_k\),我们可以写出 i 个叶子的树大于等于 \(\sum P_kP'_k\)。
我们去考虑 \(P'_k\) 是啥?我们去考虑一棵左边有 k 个叶子的树怎么得到。首先肯定左右各有一个,然后左边拓展 k-1 次,右边拓展 i-k-1 次,一共有 \({i-2\choose k-1}\) 种不同方案。然后我们考虑生成 k 个叶子的树的方案是 \((k-1)!\) 种,于是乘起来得到树的方案数,然后我们惊奇地发现它与 k 无关,于是 \(P'_1=P'_2=\dots=P'_i\)。然后就直接写成 \(\sum P_k\over i - 1\) 即可。
白雪皑皑
维护右边第一个没有染色的位置,用并查集维护即可。
signed main(){
n = rd(), m = rd(), p = rd(), q = rd();
for(int i = 1; i <= n; ++i)ff[i] = i; ff[n + 1] = n + 1;
for(int i = m; i; --i){
int l = (i * p + q) % n + 1, r = (i * q + p) % n + 1;
if(l > r)swap(l, r);
for(int j = l; j <= r; j = ff[j])if(fd(j) == j)
col[j] = i, ff[j] = fd(j + 1);
}
for(int i = 1; i <= n; ++i)wt(col[i]), pc('\n');
return 0;
}
Mex
不难但是有趣的好题,考虑用限制去确定每个数该怎么填。显然有小于 val 必须出现在 \([l,r]\) 中和 val 不能出现在 \([l,r]\) 中两个限制。于是我们对每个数分别记录能出现和不能出现的位置。然后从小到大填数即可。最后填法的正确性是因为能填数的集合要么不交要么包含,不存在其他关系,这个可以反证法轻松得到。
signed main(){
n = rd(), m = rd();
for(int i = 0; i <= n; ++i)lp[i] = rm[i] = n;
for(int i = 1, l, r, v; i <= m; ++i){
l = rd(), r = rd(), v = rd();
if(v)MAX(lm[v - 1], l), MIN(rm[v - 1], r);
else ++d[l], --d[r + 1];
MIN(lp[v], l); MAX(rp[v], r);
}
for(int i = 1; i <= n; ++i)d[i] += d[i - 1];
for(int i = n - 1; ~ i; --i){
MAX(lm[i], lm[i + 1]); MIN(rm[i], rm[i + 1]);
if(lm[i] > rm[i])return wt(- 1), 0;
}
for(int i = lm[0]; i <= rm[0]; ++i)
if(! d[i])st[++top] = i, vs[i] = true;
if(! top)return wt(- 1), 0; --top;
for(int i = lm[0]; i <= rm[0]; ++i)
if(! vs[i])st[++top] = i, vs[i] = true;
for(int i = 1; i <= n; ++i){
if(lp[i] <= rp[i]){
for(int j = lm[i]; j < lp[i] and j <= rm[i] and ! vs[j]; ++j)
st[++top] = j, vs[j] = true;
for(int j = rm[i]; j > rp[i] and j >= lm[i] and ! vs[j]; --j)
st[++top] = j, vs[j] = true;
}
if(! top or (lp[i] <= st[top] and st[top] <= rp[i]))return wt(- 1), 0;
ans[st[top--]] = i;
for(int j = max(lm[i], lp[i]); j <= rm[i] and ! vs[j]; ++j)
st[++top] = j, vs[j] = true;
for(int j = min(rm[i], rp[i]); j >= lm[i] and ! vs[j]; --j)
st[++top] = j, vs[j] = true;
}
for(int i = 0; i <= n; ++i)wt(ans[i]), pc(' ');
return 0;
}
人人尽说江南好
考虑最后的形态一定是有若干 m 和一个数,于是做完了。
Tweetuzki 爱取球
引理:若一个事件在一个时刻发生的概率为 p,则期望发生的时间为 \(1\over p\)。
我们考虑取一个球概率是 1,然后现在要从 n 个中取剩下的 n-1 个,发生概率为 \(n-1\over n\),期望时间为 \(n\over n-1\),然后以此类推即可。
最大公约数
题目要求静态区间的信息,可以想到分治解决。首先我们要明确的是对于 \(\gcd\) 相同的时候我们去选择极长的区间,然后你考虑这种区间会有多少个?因为 \(\gcd\) 的变化每次至少除以二,于是确定一个分治中心后对于一边区间端点的变化是 \(\mathcal O(\log)\) 的,于是暴力找出所有区间即可。时间复杂度 \(\mathcal O(n\log^3n)\),虽然非常丑陋但是能过。
inline void sol(int l, int r){
if(l == r)return MAX(ans, a[l]); int mid = l + r >> 1;
sol(l, mid); sol(mid + 1, r);
lm[cl = 1] = make_pair(1, mid); rm[cr = 1] = make_pair(1, r - mid);
ll t = a[mid];
for(int i = mid; i >= l and t != 1; --i, t = gcd(t, a[i]))
if(i == l or t != gcd(t, a[i - 1]))lm[++cl] = make_pair(t, mid - i + 1);
t = a[mid + 1];
for(int i = mid + 1; i <= r and t != 1; ++i, t = gcd(t, a[i]))
if(i == r or t != gcd(t, a[i + 1]))rm[++cr] = make_pair(t, i - mid);
for(int i = 1; i <= cl; ++i)for(int j = 1; j <= cr; ++j)
MAX(ans, gcd(lm[i].first, rm[j].first) * (lm[i].second + rm[j].second));
}
矩形区域
发现四个方向都有限制,但是对于左右或者上下其实是一样的。我们考虑先处理出对于列 \([l,r]\) 所有合法的行,具体处理你可以考虑一个单调栈,每次弹出的时候就是一个边界,直接记录即可。然后我们其实可以直接对行进行一个扫描线,对于一个固定的 \(r\) 考虑其上下左三个边界。
具体的,我们可以先对 \(r\) 这一列用单调栈处理出合法的区间,然后对于上下的边界记录其左边界,更新好后我们就去枚举左边界,判断上下边界有多少合法的即可。时间复杂度 \(\mathcal O(n^2)\) 或 \(\mathcal O(n^2\log n)\)。
inline void get(){
top = 0; vector < pii > ().swap(res);
for(int i = 1; i <= len; ++i){
while(top and a[i] > a[st[top]]){
if(st[top] + 1 < i)res.push_back(make_pair(st[top] + 1, i - 1));
--top;
}
if(top){
if(st[top] + 1 < i)res.push_back(make_pair(st[top] + 1, i - 1));
if(a[i] == a[st[top]])--top;
}
st[++top] = i;
}
}
inline void upd(int l, int r, int u, int d){
len = 0; for(int i = u - 1; i < d + 2; ++i)a[++len] = g[i][r];
get(); for(pii i : res)ans += lm[i.first + u - 2][i.second + u - 2] <= l;
}
const string FileName = "";
signed main(){
// fileio(FileName);
n = rd(), m = len = rd();
for(int i = 1; i <= n; ++i)for(int j = 1; j <= m; ++j)g[i][j] = rd();
for(int i = 2; i < n; ++i){
for(int j = 1; j <= m; ++j)a[j] = g[i][j]; get();
for(pii j : res)ok[j.first][j.second].push_back(i);
}
for(int i = 2; i < m; ++i){
len = n; for(int j = 1; j <= n; ++j)a[j] = g[j][i]; get();
for(pii j : res){
if(rm[j.first][j.second] + 1 < i)
lm[j.first][j.second] = i;
rm[j.first][j.second] = i;
}
for(int j = 2; j <= i; ++j)if(! ok[j][i].empty()){
auto lp = ok[j][i].begin();
for(auto cur = ok[j][i].begin(), las = cur++; cur != ok[j][i].end(); ++cur, ++las)
if(*las + 1 < *cur)upd(j, i, *lp, *las), lp = cur;
upd(j, i, *lp, ok[j][i].back());
}
}
return wt(ans), 0;
}
Euklid
困难题不会,抄的题解()后面补证明。
切树游戏
先考虑静态的问题,我们能够得到一个朴素的 dp,设 \(f_{u,i}\) 表示最高点为 i 异或和为 j 的连通块数量。转移其实很显然:
这玩意是一个异或卷积的形式于是 fwt,然后我们实际写的时候可以先做卷积从而转化成维护卷积的问题最后再还原回去,假设现在的 f 数组已经做过卷积了,我们就可以写成这个式子:\(f_{u,i}=f_{u,i}\times f_{v,i}+f_{u,i}\),化简得到 \(f_{u,i}=f_{u,i}\times(f_{v,i}+1)\)。
对于树上的操作我们可以考虑静态 toptree,我们改装一下 dp,设 \(f_{u, 0/1,0/1}\) 表示在做了卷积后在 u 的簇内选没选上下界点且至少选一个点的方案。那么现在我们需要考虑合并二度点删除一度点。这个类比广义串并联图即可。
inline poly operator + (poly a, poly b){poly s; for(int i = 0; i < m; ++i)s[i] = Add(a[i], b[i]); return s;}
inline poly operator * (poly a, poly b){poly s; for(int i = 0; i < m; ++i)s[i] = Mul(a[i], b[i]); return s;}
void FWT(poly &a, bool o){
for(int i = 1; i < m; i <<= 1)for(int j = 0; j < m; j += i << 1)
for(int k = 0; k < i; ++k){
int x = a[j | k], y = a[i | j | k];
a[j | k] = o ? Sub(x, y) : Mul(Add(x, y), i2);
a[i | j | k] = o ? Add(x, y) : Mul(Sub(y, x), i2);
}
}
inline node cps(const node &x, const node &y){return {x.l + y.l * x.len, y.r + x.r * y.len, x.m + y.m + x.r * y.l, x.len * y.len};}
inline node rak(const node &x, const node &y){return {x.l + y.l + x.l * y.l, x.r, x.m + y.m, x.len + x.len * y.l};}
inline void pu(int x){
if(! ls[x] and ! rs[x])return; siz[x] = siz[ls[x]] + siz[rs[x]];
lp[x] = lp[ls[x]]; tr[x] = ty[x] ? rak(tr[ls[x]], tr[rs[x]]) : cps(tr[ls[x]], tr[rs[x]]);
}
inline int bd(int l, int r, bool o){
if(l == r)return st[l]; int mid = l, t = 0, tot = 0, x = ++cnt;
for(int i = l; i <= r; ++i)tot += siz[st[i]];
for(; mid + 1 < r and t * 2 < tot; t += siz[st[mid++]]);
ty[x] = o; ls[x] = bd(l, mid, o); rs[x] = bd(mid + 1, r, o);
fa[ls[x]] = fa[rs[x]] = x; return pu(x), x;
}
inline void dfs1(int u){
sz[u] = 1; for(int v : e[u])if(v != Fa[u]){
Fa[v] = u; dfs1(v); sz[u] += sz[v];
if(sz[son[u]] < sz[v])son[u] = v;
}
}
inline void dfs2(int u){
int l = top + 1, rt, x; if(u != 1)st[++top] = u;
for(int v = u; son[v]; v = son[v]){
st[rt = ++top] = son[v];
for(int w : e[v])if(w != Fa[v] and w != son[v])dfs2(w);
x = bd(rt, top, 1); st[top = rt] = x;
}
x = bd(l, top, 0); st[top = l] = x;
}
signed main(){
ios :: sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
cin >> n >> m; for(int i = 0; i < m; ++i)g[i][i] = 1, FWT(g[i], 1);
for(int i = 1; i <= n; ++i)cin >> a[i];
for(int i = 1, u, v; i < n; ++i)cin >> u >> v, e[u].push_back(v), e[v].push_back(u);
dfs1(1); cnt = n; for(int i = 2; i <= n; ++i)siz[i] = 1, tr[i] = {g[a[i]], g[a[i]], g[a[i]], g[a[i]]}, lp[i] = Fa[i];
dfs2(1); root = st[1];
for(cin >> q; q--; ){
char c[10]; int x; cin >> c >> x;
if(c[0] == 'Q'){
poly res = (tr[root].l * g[a[lp[root]]]) + tr[root].m;
FWT(res, 0); ADD(res[a[lp[root]]], 1); cout << res[x] << endl;
}
else{
cin >> a[x]; tr[x] = {g[a[x]], g[a[x]], g[a[x]], g[a[x]]};
for(x = fa[x]; x; x = fa[x])pu(x);
}
}
return 0;
}
无向图三元环计数
不会 \(\mathcal O({n^3\over\omega})\) 做法/ll,但是会 \(\mathcal O(m\sqrt m)\) 做法。
考虑给边定向,从度数小的点连向度数大的点,然后以度数为基础去枚举出边更新每个点最近能跳到哪里,然后重新扫一遍顺便枚举一下边判断即可。
时间复杂度证明考虑分讨度数,如果原来就小于等于 \(\sqrt m\) 那么出度一定小于等于 \(\sqrt m\);否则这样的点只有 \(\mathcal O({\sqrt m})\) 个,于是新图上的出度也只有 \(\mathcal O({\sqrt m})\) 于是得证。
signed main(){
n = rd(), m = rd();
for(int i = 1; i <= m; ++i)++d[a[i] = rd()], ++d[b[i] = rd()];
for(int i = 1; i <= m; ++i){
if(d[a[i]] > d[b[i]] or (d[a[i]] == d[b[i]] and a[i] > b[i]))swap(a[i], b[i]);
e[a[i]].push_back(b[i]);
}
for(int i = 1; i <= n; ++i){
for(int j : e[i])las[j] = i;
for(int j : e[i])for(int k : e[j])ans += las[k] == i;
}
return wt(ans), 0;
}
数字串拆分
数位 dp 牛牛题。
首先最基础的 dp 式子非常显然,就是“爬楼梯”状物,然后你发现数字比较大于是矩阵快速幂启动!于是我们就轻松解决了 f,现在考虑 g,我们其实可以先预处理每个数字在不同数位上的值,这样我们拼起来的时候就不用重新计算。
然后我们就类似数位 dp 一样去做,只不过这个题每一位已经确定了,所以我们可以用一个 \(\mathcal O(n^2)\) 的枚举处理出数位 \([l,r]\) 的值然后合并。
signed main(){
ios :: sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
cin >> s + 1 >> m; n = strlen(s + 1); f[0][0][0] = 1;
for(int i = 0; i < m; ++i)bs[i][0] = 1;
for(int i = 1; i < m; ++i)bs[i - 1][i] = 1;
for(int i = 0; i < 10; ++i){
a[0][i] = qmi(bs, i);
for(int j = 1; j <= n; ++j)a[j][i] = qmi(a[j - 1][i], 10);
}
for(int j = n; j; --j)for(int i = j; i; --i)
if(i < j)b[i][j] = b[i + 1][j] * a[j - i][s[i] ^ 48];
else b[i][j] = a[0][s[i] ^ 48];
for(int i = 1; i <= n; ++i)for(int j = 0; j < i; ++j)
f[i] = f[i] + f[j] * b[j + 1][i];
return cout << f[n][0][0], 0;
}
禁忌
如果串比较短那不就是 AC 自动机的板子吗?只不过每次向子节点转移的时候概率还要乘一个 \(1\over|S|\),其中 \(|S|\) 是字符集大小。具体的,设 \(f_{i,j}\) 表示长度为 \(i\) 且匹配到节点 \(j\) 的概率。转移时考虑下一个点是否是终止点,是的话有 \(f_{i,1}\leftarrow {f_{i-1,j}+1\over |S|}\),否则有 \(f_{i,ch_{j,c}}\leftarrow {f_{i-1,j}\over |S|}\)。统计答案就是累加每个终止点处的 dp 值即可。
考虑文本串长度限制很长的时候怎么办?你也许能发现:我们的转移方程与长度其实没有关系,它只需要考虑现在在 AC 自动机上的哪个点。所以我们直接把 dp 写成矩阵形式直接矩阵快速幂即可。
signed main(){
scanf("%d %d %d", &n, &k, &V);
for(int i = 1; i <= n; ++i)scanf("%s", s), ins(s);
getfail();
for(int i = 1; i <= m; ++i)for(int j = 0; j < V; ++j)
if(ed[ch[i][j]])A.a[i][1] += 1.0 / V, A.a[i][m + 1] += 1.0 / V;
else A.a[i][ch[i][j]] += 1.0 / V;
++m; A.a[m][m] = 1; for(int i = 1; i <= m; ++i)ans.a[i][i] = 1;
for(; k; k >>= 1, A = A * A)if(k & 1)ans = ans * A;
return printf("%.10Lf\n", ans.a[1][m]), 0;
}
花园
状压 dp 直接做?诶这个 n 怎么这么大?于是用矩阵维护转移做快速幂即可。
aliens
一个比较好的转化与一点基础的支配!
将二维平面转化为区间问题,发现有限制直接上 wqs 二分,于是可以做了,这题我会!
设 \(f_i\) 表示区间右端点选到 i 时的最少方格数,转移枚举左端点。预处理一下每个纵坐标下的最小横坐标,这样支配一下就能够比较容易写出转移方程了:
其中 mid 是二分的限制,\(g_k\) 是支配后每个纵坐标下的最小横坐标,转移方程是这样的因为你先去考虑左端点落在哪里,然后容斥算新覆盖的小方格。然后你稍微拆一下式子就能斜率优化做了。
inline int calc(){
q[hd = tl = 1] = 0;
for(int i = 1; i <= n; ++i){
while(hd < tl and sp(q[hd], q[hd + 1]) + eps <= 2 * a[i].r)++hd;
int j = q[hd]; f[i] = f[j] + sq(a[i].r - a[j + 1].l) - dt[j] - mid; g[i] = g[j] + 1;
if(i < n)while(hd < tl and sp(q[tl - 1], q[tl]) - eps >= sp(q[tl], i))--tl;
q[++tl] = i;
}
return g[n];
}
signed main(){
n = rd(), m = rd(), k = rd();
for(int i = 1, x, y; i <= n; ++i)x = rd(), y = rd(), a[i] = {min(x, y), max(x, y)};
sort(a + 1, a + 1 + n); int tot = 0;
for(int i = 1, j = - 1; i <= n; ++i)if(a[i].r > j)j = a[i].r, a[++tot] = a[i]; n = tot;
for(int i = 1; i < n; ++i)if(a[i].r >= a[i + 1].l)dt[i] = sq(a[i].r - a[i + 1].l + 1);
for(int i = 1; i <= n; ++i)++a[i].r;
int l = - 1e12, r = 0, res = 0;
while(l <= r){
mid = l + r >> 1;
if(calc() <= k)res = mid, l = mid + 1; else r = mid - 1;
}
mid = res; calc(); wt(f[n] + mid * k); return 0;
}
树根
优先考虑深度最深的点,然后想怎么搞最合理,是不是在限制-1级祖先处添加新的边,然后相当于这个子树就都合法了。于是有了大体思路。我们先去二分限制,然后每次找到深度最大的点,还需要求一下 k 级祖先,再删掉一个子树。发现前面的东西需要倍增,后面需要线段树,然后做完了。考虑换根怎么办?我们需要分讨一下根与操作的点的位置关系,然后也做完了,但是需要注意这里不能二分,不然就是两只 \(\log\)。其实可以注意到每次移动一步限制变化不超过 1,感觉可以用反证法证一下,知道这个后就枚举相邻的限制于是只有一只 \(\log\) ,跑得飞快!
inline bool chk(int rt, int lim){
bool ok = true; int cnt = 0;
while(ma[1] > lim + 1){
if(++cnt > k){ok = false; break;}
int x = kth(get(1, 1, n), rt, lim - 1);
if(dfn[x] < dfn[rt] and dfn[rt] < dfn[x] + sz[x]){
mdf(1, 1, n, 1, n, - n); int u = kfa(rt, dep[rt] - dep[x] - 1);
mdf(1, 1, n, dfn[u], dfn[u] + sz[u] - 1, n); ms.push_back(- u);
}
else mdf(1, 1, n, dfn[x], dfn[x] + sz[x] - 1, - n), ms.push_back(x);
}
while(! ms.empty()){
int y = ms.back(), x = abs(y);
mdf(1, 1, n, dfn[x], dfn[x] + sz[x] - 1, n * (x / y));
if(y < 0)mdf(1, 1, n, 1, n, n); ms.pop_back();
}
return ok;
}
inline void dfs2(int u, int fa){
if(u != 1)mdf(1, 1, n, 1, n, 1), mdf(1, 1, n, dfn[u], dfn[u] + sz[u] - 1, - 2);
if(u == 1){
int l = 1, r = n, res = 0;
while(l <= r){int mid = l + r >> 1; if(chk(u, mid))res = mid, r = mid - 1; else l = mid + 1;}
ans[u] = res;
}
else for(int i = max(1, ans[fa] - 1); i <= min(n, ans[fa] + 1); ++i)if(chk(u, i)){ans[u] = i; break;}
for(int i = hd[u], v; i; i = e[i].nxt)if((v = e[i].to) != fa)dfs2(v, u);
if(u != 1)mdf(1, 1, n, 1, n, - 1), mdf(1, 1, n, dfn[u], dfn[u] + sz[u] - 1, 2);
}
signed main(){
n = rd(), k = rd(), ty = rd();
for(int i = 1, u, v; i < n; ++i)u = rd(), v = rd(), add(u, v), add(v, u);
dfs1(1, 0); bd(1, 1, n); dfs2(1, 0);
wt(ans[1]); if(! ty)return 0;
for(int i = 2; i <= n; ++i)pc(' '), wt(ans[i]);
return 0;
}

浙公网安备 33010602011771号