长存不灭的过去,逐渐消失的未来。|

Iron_Spade

园龄:6个月粉丝:1关注:2

妙妙 trick 题

所有链接基本都是 luogu 链接。

大部分题是一个题单里的,小部分题可能是新知比较有意义的题目。

大多是 CF 的,不是 CF 的基本都是新知,也写进来。

2025/1/20

CF1292B(*1700)

评分个人感觉有点低了,毕竟有点偏思维。

注意到 1t1016,并且点的坐标满足一次函数形式,说人话就是点与点之间的间隔会越来越大。

题目给定了起点 stxsty,发现走的方式要么向着 p0 走,要么向着 p 走,如果向着后者走的话,点间距越来越大,显然不合题意,应该贪心往 p0 方向走,然后再走回去,这样的话答案显然是最优的,题解给出了一大坨证明但我认为不需要,显然这样走是唯一的走法,因为你不可能不走到头就直接回头往 p 方向走。

CF1396B(*1800)

看上去是博弈,实际上确实是博弈,因为一个人可以选择一堆拿一个并且锁掉这一堆,所以发现如果最开始有一堆的数量大于其余所有堆的总数,那么先手必胜,策略是先手一直锁定这一堆。

如果没有的话,二者一定会选择当前最大的一堆并锁住,这样到最后一定会拿完所有的石头,所以直接判奇偶即可。

2025/1/21

P4008(Splay 新知)

平衡树求第 k 个节点的编号,或者是说第 k 大的节点编号。

点击查看
int get_kth(int v) {
    int x = rt;
    while(x) {
      int sum = siz(ls(x)) + 1;
      if(v == sum) return x;
      if(v < sum) x = ls(x);
      else v -= sum, x = rs(x);
   }
}

2025/1/22

CF1748E(*2300)(笛卡尔树新知)

栈顶为树根,一般会定义 dpx,y 表示以 x 为根的子树加了个 y 限制怎么怎么样,这个题是定义 dpx,y 表示以 x 为根其权值为 y 的方案数。

然后因为笛卡尔树有堆的性质所以一段区间最小值 minlirai=alca(l,r),若对于所有的区间这个东西(lca)都相同说明这棵树的形态固定。直接建树 dp 即可。

点击查看
void dfs(int u) {
	rep(i, 1, k) dp[u][i] = 1;
	if(ch[u][0]) dfs(ch[u][0]);
	if(ch[u][1]) dfs(ch[u][1]);
	if(ch[u][0]) rep(i, 1, k) dp[u][i] = dp[u][i] * dp[ch[u][0]][i - 1] % mod;
	if(ch[u][1]) rep(i, 1, k) dp[u][i] = dp[u][i] * dp[ch[u][1]][i] % mod;
	rep(i, 2, k) dp[u][i] = (dp[u][i] + dp[u][i - 1]) % mod;
}

CF2048F(*2500)(笛卡尔树新知)

gcd 差不多,即使每次都取 [1,n] 的区间做操作,那么整个区间的值每次至少都会减半,则答案为 log 级别。

如上道题目所述一样,套路地定义 dpu,i 表示以 u 为根做了 i 次操作之后其子树的最大值的最小值能是多少。答案即为 min1i64(dprt,i=0)

ABC341F

这个 blog 标题可以删掉 CF 二字了。

傻逼 trick 题,首先知道对于一条边 (u,v),必须要有 wu>wvu 才能向 v 传递,于是边其实是单向的。发现 a 的值域很大猜测每个点的传递方式是固定的,实则也确实是这样。

考虑怎么算传递集合 Su,一个很精妙的想法是直接定义 gi 表示第 i 个点传递一次的贡献的最大值,最后的答案即为 i=1ngi×ai。然后先转移 wi 小的,用朴素背包转移即可。

点击查看
rep(i, 1, n) {
    memset(f, 0, sizeof f);
    u = id[i];
    for(auto v : e[u]) {
        if(w[v] < w[u]) {
            pre(j, w[u] - 1, w[v]) f[j] = max(f[j], f[j - w[v]] + g[v]);
        }
    }
    rep(j, 0, N - 5) g[u] = max(g[u], f[j] + 1);
}

2025/1/24

P3043

有一个长度为 n 的 0/1 序列,给你 m 条限制,每条限制为一个二元组 (u,v) 表示 uv 中一个为 0 一个为 1,求原序列的方案数对 109+7 取模。

TQ 翻译:给每条边找一个配对的点,要求边 (u,v) 配对的点是 uv,且每个点最多只能被一条边配对,求不同方案数。

🐕⑩ 人类智慧题,考虑三种情况:

先挖掘性质:一个环的贡献为 2,显然环上的点可以选左边或者选右边。

  • 环:是棵基环树,先选环,每个点可以选左边或者选右边,其余点方案唯一,总贡献为 2
  • 树:会多出一个点不会被选择,当然是随便选一个点,其余点方案唯一,贡献为 sizi
  • 边数大于点数的连通块:一定有点会被同时选择,不合题意,答案为 0

根据乘法原理即可,答案为 i=1kwi

ABC348G

这个 trick 做过的,但是考场上因为好同学 Xdik 加上对整场题目的判断有严重错误在处于摆烂状态没认真想

题目是求每个 k 要求选出一个大小为 k 的集合 S 满足 uSaumaxuSbu 最大。

两者都有 uS 的性质并且根据有场 abc 的 E 有个 trick 是直接钦定一个最大值,然后在剩下几个 b 比其小的数中选 k1ai 最大的,然后这个所有 k 是可以一起求的。求前 k1 大可以在主席树上维护前 k 大前缀和,剩下就是板子。

然后一起求的话可以分治优化一下,时间复杂度是两只 log

点击查看
int upd(int id, int l, int r, int x) {
	int p = ++tot; t[p] = t[id];
	++v(p), s(p) += b[x];
	if(l == r) return p;
	int mid = l + r >> 1;
	if(x <= mid) ls(p) = upd(ls(id), l, mid, x);
	else rs(p) = upd(rs(id), mid + 1, r, x);
	return p;
}

int ask(int p, int l, int r, int k) {
	if(l == r) return k * b[l];
	int mid = l + r >> 1;
	if(v(rs(p)) >= k) return ask(rs(p), mid + 1, r, k);
	else return s(rs(p)) + ask(ls(p), l, mid, k - v(rs(p)));
}

int w(int k, int x) {
	return ask(rt[x], 1, V, k) - a[x].fi;
}

CF1495E(*3200)

先咕。

2025/1/25

因为🐕 guomao 回归了然后没时间做了咕咕咕。

2025/2/4

因为放假回家过年了今天才回归。

CF827F(*3200)

拆点,我的题解

Para 上课的时候一个很纽币的题感觉自己听懂了就写了其实也很好写。

CF888G(*2300)

暴力时空都是 O(N2) 的,正解是挖掘异或性质,将这些数挂到 trie 上然后 dfs 转移区间,然后不会了,贺的🔪八的。

放下代码:

点击查看
#include <bits/stdc++.h>
#define FASTIO ios::sync_with_stdio(0), cin.tie(nullptr), cout.tie(nullptr);
#define rep(i, j, k) for(int i = j; i <= k; ++i)
#define pre(i, j, k) for(int i = j; i >= k; --i)
#define pb push_back
#define int long long
using namespace std;
const int N = 2e5 + 5;

int n, a[N];

namespace Trie {
	int rt, tot, ch[2][N << 5], l[N << 5], r[N << 5];
	void ins(int& p, int x, int d) {
		if (!p) p = ++tot;
		if (!l[p]) l[p] = x;
		r[p] = x;
		if (d >= 0) ins(ch[a[x] >> d & 1][p], x, d - 1);
	}
	int get(int p, int x, int d) {
		return (d < 0 ? 0 : ch[x >> d & 1][p] ? get(ch[x >> d & 1][p], x, d - 1) : get(ch[!(x >> d & 1)][p], x, d - 1) + (1 << d));
	}
	int dfs(int p, int d) {
		if (!p || d < 0) return 0;
		int ret = 1e18;
		if (ch[0][p] && ch[1][p]) rep(i, l[ch[0][p]], r[ch[0][p]]) ret = min (ret, get(ch[1][p], a[i], d - 1) + (1 << d));
		return dfs(ch[0][p], d - 1) + dfs(ch[1][p], d - 1) + ret * (ch[0][p] && ch[1][p]);
	}
}
using namespace Trie;

signed main() {
	FASTIO

	cin >> n;
	rep(i, 1, n) cin >> a[i];
	sort(a + 1, a + 1 + n);
	rep(i, 1, n) ins(rt, i, 30);
	cout << dfs(rt, 30);
	return 0;
}

2025/2/5

P5437

一个完全图有 nn2 棵生成树,一条边在生成树中会出现 nn2×(n1)n×(n1)2=2×nn3 次。

所以最小生成树的边权和的期望就可以用拉插来做,这个题比较板。

变形一下即可求出答案是 2n×i=1nj=i+1n(i+j)k

乱搞一下可得:

12×i=1nj=1n(i+j)k2k×i=1nik

实际上这显然是没必要的,先将 2n 扔出去,然后记 En=i=1nj=i+1n(i+j)k,找通项是这类题的一个很纽币的 trick,一般找通项的方法是差分,考虑求出 EnEn1=i=n+1n×21ik,然后发现又是 f(k)=xk 的形式,根据前人告诉我们这是一个 k+1 次的多项式,又因为差分会降一次,故 En 是一个 k+2 次的多项式,考虑大力插值,插 k+3 个即可,用线性筛拿几个出来就可以了。

注意不要写这个东西,因为最大可能有 107+3

const int N = 1e7 + 5;
rep(i, 1, N - 5) ...
点击查看
inline void init(int n) {
	p[1] = 1;
	rep(i, 2, n) {
		if(!vis[i]) pr[++m] = i, p[i] = qkpow(i, k);
		for(int j = 1; j <= m && 1LL * i * pr[j] <= n; ++j) {
			vis[1LL * i * pr[j]] = 1;
			p[1LL * i * pr[j]] = 1LL * p[i] * p[pr[j]] % mod;
			if(i % pr[j] == 0) break;
		}
	}
	rep(i, 2, n) p[i] = (p[i - 1] + p[i]) % mod;
}


inline void init_pv(int n, int nn) {
	pre[0] = suf[0] = pre[n + 1] = suf[n + 1] = 1LL;
	rep(i, 1, n) pre[i] = suf[i] = nn - i;
	rep(i, 1, n) pre[i] = 1LL * pre[i - 1] * pre[i] % mod;
	pre(i, n, 1) suf[i] = 1LL * suf[i + 1] * suf[i] % mod;
	rep(i, 2, n) y[i] = ((1LL * y[i - 1] + p[i * 2 - 1] - p[i]) % mod + mod) % mod;
}

int Lagrange(int n) {
	int ret = 0;
	rep(i, 1, n) {
		int p = 1LL * pre[i - 1] * suf[i + 1] % mod * inv[i - 1] % mod * inv[n - i] % mod;
		if(n - i & 1LL) p = mod - p;
//		cout << pre[i - 1] << ' ' << suf[i + 1] << ' ' << inv[i - 1] << ' ' << inv[n - i] << '\n';
		ret = (ret + 1LL * p * y[i] % mod) % mod;
	}
	return ret;
}

2025/2/6

FFT 新知啊,由后到前写。

P3723

其实题目中一堆批话都是想让你求这个东西:

minmcmi=1n(aibi+c)2

直接拆开得到

i=1nai2+bi2+c2+c×(aibi)2×ai×bi

故求

maxmcmi=1nai×bi

发现除了 2×ai×bi 这一项其他都是确定的,直接 copy a 数组断环做 FFT 即可。

upd on 2025/2/10

ABC392G

哈哈场上通过将近 1000 人什么水平的题,具体见不多评价了。

2025/2/7

NTT 新知啊。

CF1096G(*2400)

纽币的题,构造多项式 g(x)=u=1500000pu×xu,其中 pu=ud

定义一个朴素 dp,dpi,j 表示前 i 个数和为 j 的方案数,则有转移

dpi,j=u=1jdpi1,ju×pu

则答案为

u=1500000dpn2,u

然后你发现 dpi 其实就是 dpi1g 的卷积,你又发现 dp0={1,0,00},说明任何多项式和 dp0 做卷积都会得到原多项式,则 dpn2 其实就是 g(x)n2

然后就比较板了,现在开始写。

写的过程中比较顺利。

哦对了因为这个快速幂到最后多项式最多只有 5n 项所以它是对的,这个快速幂不一定适用于任何场合

点击查看
void sol() {
	NTT(p, tot, 1);
	rep(i, 0, tot) p[i] = qkpow(p[i], n / 2); 
	NTT(p, tot, -1);
	int ret = 0;
	rep(i, 0, tot) ret += p[i] * p[i], ret %= mod;
	cout << ret << '\n';
}

完整 code

2025/2/7

write on 2/6:明天 Rainybunny 要讲纽币的 dp,但多半是看题解,可能不会去听,应该是继续补 FFT/NTT 的题。

CF528D(*2500)

套路题,这种偏差不超过 k 个单位相当于向左右扩散 k 个,剩下基本就是套路,有篇题解的一段代码最开始没搞懂为啥,现在发现好唐致远啊,粘下那段 code。

for (int i = 0, lst = -1e9; i < n; i++) {
	if (s[i] == c)
		lst = i;    // 找左边最近的当前字母 c.
	if (i - lst <= k)
		f.a[i] = 1;    // 如果距离合法, 则设 1.
}
for (int i = n - 1, lst = 1e9; ~i; i--) {
	if (s[i] == c)
		lst = i;    // 找右边最近的当前字母 c.
	if (lst - i <= k)
		f.a[i] = 1;    // 如果距离合法, 则设 1.
}

写的也很顺利,完整 code

P3321

log 化乘为加转化为卷积形式的 trick,代码贺的 Xdik 的没写。

code

2025/2/10

今日 crashed 讲 ds,听不懂一点但还是选了几道题做一下。

「雅礼集训 2017 Day1」市场

主要是有个操作是区间除(向下取整)不好整,考虑势能线段树,你发现每次除只会影响区间 max,所以考虑同时计算 max 的影响并由此确定是否继续向下递归。

void upd_div(int p, int ql, int qr, int k, int l = 1, int r = n) {
	if(ql <= l && r <= qr) {
		int px = mx[p] - floor(1.0 * mx[p] / k), py = mn[p] - floor(1.0 * mn[p] / k);
		if(px == py) return s[p] -= (r - l + 1) * px, add[p] -= px, mn[p] -= px, mx[p] -= px, void();
	}
	down(p, l, r);
	if(ql <= mid) upd_div(ls, ql, qr, k, l, mid);
	if(qr > mid) upd_div(rs, ql, qr, k, mid + 1, r);
	pushup(p);
}

青蛙题 Frog(rmscne)

破案了这道题是搬的 [Ynoi2005] rmscne

题目描述:先咕。
一个子段可以看作后缀的前缀,然后做扫描线,然后不会了看的第一篇题解,粘下认为比较重要的部分 code。

点击查看
#include <bits/stdc++.h>
#define FASTIO ios::sync_with_stdio(0), cin.tie(nullptr), cout.tie(nullptr);
#define rep(i, j, k) for(int i = j; i <= k; ++i)
#define pre(i, j, k) for(int i = j; i >= k; --i)
#define pb push_back
#define PII pair<int, int>
#define fi first
#define se second

using namespace std;
const int N = 2e6 + 5;

int n, q, l, r, a[N], fa[N], lst[N], ans[N];
inline int get(int x) { return x == fa[x] ? x : fa[x] = get(fa[x]); }
void merge(int val) { if(lst[val]) fa[lst[val]] = lst[val] + 1; }

namespace SegmentTree {
	int mn[N << 2], add[N << 2];
	#define ls (p << 1)
	#define rs (p << 1 | 1)
	#define mid (l + r >> 1)
	void pushup(int p) { mn[p] = min(mn[ls], mn[rs]); }
	void down(int p, int l, int r) {
		if(!add[p]) return;
		add[rs] = mn[rs] = add[p], add[ls] = mn[ls] = add[p] + (r - mid);
//		cout << '!' << add[ls] << ' ' << add[rs] << '\n';
		add[p] = 0;
	}
	void build(int p, int l, int r) {
		if(l == r) return mn[p] = 1e9, void();
		build(ls, l, mid), build(rs, mid + 1, r), pushup(p);
	}
	void upd(int p, int ql, int qr, int &x, int l = 1, int r = n) {
//		cout << '*' << x << '\n';
		if(ql <= l && r <= qr) return x -= (r - l + 1), mn[p] = add[p] = x + 1, void();
		down(p, l, r);
		if(ql <= mid) upd(ls, ql, qr, x, l, mid);
		if(qr > mid)  upd(rs, ql, qr, x, mid + 1, r);
		pushup(p);
	}
	int ask(int p, int ql, int qr, int l = 1, int r = n) {
		if(ql <= l && r <= qr) return mn[p];
		down(p, l, r);
		int ret = 1e9;
		if(ql <= mid) ret = min(ret, ask(ls, ql, qr, l, mid));
		if(qr > mid)  ret = min(ret, ask(rs, ql, qr, mid + 1, r));
		return ret;
	}
} 
using namespace SegmentTree;
vector<PII> vec[N];

signed main() {
	FASTIO
	
	cin >> n, build(1, 1, n);
	rep(i, 1, n) cin >> a[i], fa[i] = i;
	cin >> q;
	rep(i, 1, q) cin >> l >> r, vec[r].pb({l, i});
	rep(i, 1, n) {
		int pp = i - lst[a[i]];
//		cout << '&' << p[a[i]] + 1 << ' ' << i << '\n';
		upd(1, lst[a[i]] + 1, i, pp), merge(a[i]);
		for(auto ed : vec[i]) ans[ed.se] = ask(1, ed.fi, get(ed.fi));
		lst[a[i]] = i;
	}
	rep(i, 1, q) cout << ans[i] << '\n';
	return 0;
}

2025/2/11

P3180

题意是给一颗仙人掌,q 次询问,每次询问问你断掉 1x 的所有简单路径后,y 能到达的点出现的次数为奇数的颜色数或者出现次数偶数的颜色数。

按套路建出圆方树后,由于 1 是根,断掉 1x 的简单路径等价于将 x 上面的部分断掉,保留自己子树部分,于是就很显然是 dsu on tree 了。

然后你会发现 tarjan 的过程可以直接合并答案,实现起来很简单。

点击查看
#include <bits/stdc++.h>
#define FASTIO ios::sync_with_stdio(0), cin.tie(nullptr), cout.tie(nullptr);
#define rep(i, j, k) for (int i = j; i <= k; ++i)
#define pre(i, j, k) for (int i = j; i >= k; --i)
#define pb push_back
#define PII pair<int, int>
#define fi first
#define se second

using namespace std;

const int N = 2e6 + 5;
const int M = 6e6 + 5;

int n, m, q, u, v, op, mx, ans[N], w[N];

int head[N], nxt[N], to[N], tt;
void add(int u, int v) {
	to[++tt] = v, nxt[tt] = head[u], head[u] = tt;
}

namespace SegmentTree {
	int ls[M], rs[M], odd[M], even[M], rt[N];
	#define mid (l + r >> 1)
	int cnt = 0;

	void pushup(int p) {
		odd[p] = even[p] = 0;
		if(ls[p]) odd[p] += odd[ls[p]], even[p] += even[ls[p]];
		if(rs[p]) odd[p] += odd[rs[p]], even[p] += even[rs[p]];
	}

	void upd(int &p, int l, int r, int k) {
		if(!p) p = ++cnt;
		if(l == r) return odd[p] = 1, void();
		if(k <= mid) upd(ls[p], l, mid, k);
		else upd(rs[p], mid + 1, r, k);
		pushup(p);
	}

	int merge(int p, int q, int l, int r) {
		if(!p || !q) return p | q;
		if(l == r) {
			if(even[p] && even[q]) return p;
			if(odd[p] && even[q]) return p;
			if(even[p] && odd[q]) return q;
			odd[p] = 0, even[p] = 1;
			return p;
		}
		ls[p] = merge(ls[p], ls[q], l, mid), rs[p] = merge(rs[p], rs[q], mid + 1, r);
		pushup(p);
		return p;
	}
	
	int ask(int p, int l, int r, int ql, int qr, int op) {
//		cout << '!' << l << ' ' << r << '\n';
		if(!p || ql > r || l > qr) return 0;
		if(ql <= l && r <= qr) return (op ? odd[p] : even[p]);
		return ask(ls[p], l, mid, ql, qr, op) + ask(rs[p], mid + 1, r, ql, qr, op);
	}
}
using namespace SegmentTree;

int dfn[N], low[N], stk[N], top, T, S;

struct Query {
	int x, id, type;
};
vector<Query> vec[N];

void tarjan(int x) {
	dfn[x] = low[x] = ++T;
	stk[++top] = x;
	upd(rt[x], 0, mx, w[x]);
	for(int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if(!dfn[y]) {
			tarjan(y), low[x] = min(low[x], low[y]);
			if(dfn[x] == low[y]) {
				int z = 0;
				do {
					z = stk[top--];
//					cout << '*' << rt[x] << ' ' << rt[z] << '\n';
					merge(rt[x], rt[z], 0, mx);
				} while(y != z);
			}
		}
		else low[x] = min(low[x], dfn[y]);
	}
	for(auto ed : vec[x]) ans[ed.id] = ask(rt[x], 0, mx, 0, min(ed.x, mx), ed.type);
}


signed main() {
	FASTIO
	cin >> n >> m;
	rep(i, 1, n) cin >> w[i], mx = max(mx, w[i]);
	rep(i, 1, m) cin >> u >> v, add(u, v), add(v, u);
	cin >> q; 
	rep(i, 1, q) cin >> op >> u >> v, vec[u].pb({v, i, op});
	tarjan(1);
	rep(i, 1, q) cout << ans[i] << '\n';
	return 0;
}

本文作者:Iron_Spade

本文链接:https://www.cnblogs.com/walrus908424/p/18708105

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Iron_Spade  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起