Say 题选记(10.19 - 10.25)

P3702 [SDOI2017] 序列计数

首先至少 1 个质数可以容斥成随便选 - 只选合数。然后注意到第二维很小,直接矩阵快速幂即可。

Code
#include <bits/stdc++.h>
using namespace std;
const int M = 2e7 + 5, K = 1e2 + 5, mod = 20170408;
typedef long long ll;
int n, m, p, pri[M], cnt;
ll num[K], npri[K];
bitset<M> vis;
void add(ll &x, ll y){ (x += y) %= mod;}
struct matrix{
	ll m[K][K];
	matrix(){ memset(m, 0, sizeof(m)); }  
	matrix operator * (const matrix &x){
		matrix ret;
		for(int i = 0; i < p; ++i){
			for(int j = 0; j < p; ++j){
				for(int k = 0; k < p; ++k){
					add(ret.m[i][j], m[i][k] * x.m[k][j]);
				}
			} 
		} 
		return ret;
	}
}a, b, f; 
void init(){
	vis[1] = 1;
	for(ll i = 2; i <= m; ++i){
		if(!vis[i]) pri[++cnt] = i;
		for(ll j = 1; j <= cnt; ++j){
			if(i * pri[j] > m) break;
			vis[i * pri[j]] = 1;
			if(i % pri[j] == 0) break;
		}
	}
}
matrix qpow(matrix a, int b){
	matrix ret;
	for(int i = 0; i < p; ++i) ret.m[i][i] = 1;
	for(; b; b >>= 1, a = a * a){
		if(b & 1) ret = ret * a;
	}
	return ret;
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n >> m >> p;
	init();
	for(int i = 1; i <= m; ++i){
		num[i % p]++;
		if(vis[i]) npri[i % p]++;
	} 
	for(int i = 0; i < p; ++i){
		for(int j = 0; j < p; ++j){
			a.m[i][j] = num[(j - i + p) % p];
			b.m[i][j] = npri[(j - i + p) % p];
		}
	}
	f.m[0][0] = 1;
	matrix A = f * qpow(a, n), B = f * qpow(b, n);
	cout << (A.m[0][0] - B.m[0][0] + mod) % mod;
	return 0;
}

P5358 [SDOI2019] 快速查询

注意一个细节。对于单点赋值,由于我们最后查单点的时候会带上标记,也就是 \(a_i \times mult + addt\)。而对这个点进行赋值的时候,以前的标记不应产生影响,因此应该把其赋为 \(\frac{v - addt}{mult}\)

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 1e7 + 19;
unordered_map<int, int> a;
int val, sum, addt, mult = 1, n, q, t, inv[mod];
void assign(int k){
	k = (k % mod + mod) % mod;
	a.clear();
	val = k, sum = 0, addt = 0, mult = 1;
}
void mul(int k){
	k = (k % mod + mod) % mod;
	if(k == 0) return assign(0);
	(addt *= k) %= mod, (mult *= k) %= mod;
	(val *= k) %= mod, (sum *= k) %= mod; 
}
void add(int k){
	k = (k % mod + mod) % mod;
	(addt += k) %= mod;
	(val += k) %= mod;
	(sum += 1ll * a.size() * k) %= mod;
}
int qry(int x){
	if(!a.count(x)) return val;
	return (a[x] * mult % mod + addt) % mod;
}
int qry(){ return ((val * (n - 1ll * a.size()) % mod + sum) % mod + mod) % mod; }
void upd(int x, int k){
	k = (k % mod + mod) % mod;
	if(a.count(x)) (sum += k - qry(x) + mod) %= mod;
	else (sum += k) %= mod;
	mult = (mult % mod + mod) % mod;
	a[x] = ((k - addt + mod) % mod * inv[mult]) % mod;
}
struct op{
	int typ, x, k;
}Op[100005];
int x[105], y[105];
signed main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n >> q;
	inv[1] = 1;
	for(int i = 2; i < mod; ++i){
		inv[i] = ((mod - mod / i) * inv[mod % i]) % mod;
	}
	for(int i = 1; i <= q; ++i){
		cin >> Op[i].typ;
		if(Op[i].typ != 6){
			cin >> Op[i].x;
			if(Op[i].typ == 1) cin >> Op[i].k;
		}
	}
	cin >> t;
	for(int i = 1; i <= t; ++i) cin >> x[i] >> y[i];
	int ans = 0;
	for(int i = 1; i <= t; ++i){
		for(int j = 1; j <= q; ++j){
			int idx = (x[i] + j * y[i]) % q + 1;
			switch(Op[idx].typ){
				case 1: upd(Op[idx].x, Op[idx].k); break;
				case 2: add(Op[idx].x); break;
				case 3: mul(Op[idx].x); break;
				case 4: assign(Op[idx].x); break;
				case 5: (ans += qry(Op[idx].x)) %= mod; break;
				case 6: (ans += qry()) %= mod; break;
			} 
		}
	}
	cout << (ans % mod + mod) % mod;
	return 0;
}

P3230 [HNOI2013] 比赛

比较人类智慧的爆搜题。当比赛人数固定并且给定最终分数时,最终的方案数是固定的。因此可以考虑记忆化,边搜边把这个存下来。具体来说我们把最终分数进制哈希一下(由于一个人最多 27 分,30进制就行),然后注意还要把不同人数的区分开来,对于 0 的情况我们要进行占位,解决方法是全体 +1,避免出现 0。复杂度玄学。

Code
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
typedef unsigned long long ull;
map<ull, int> f;
int n, b[15];
int dfs(int x, int y){
	if(x == n){ return b[x] == 0; }
	if(y > n){ 
		if(b[x] != 0) return 0; 
		vector<int> tmp;
		for(int i = x + 1; i <= n; ++i) tmp.emplace_back(b[i]);
		sort(tmp.begin(), tmp.end());
		ull hsh = 0;
		for(int i = 0; i < tmp.size(); ++i) hsh = hsh * 29 + tmp[i] + 1; 
		if(f.find(hsh) == f.end()) f[hsh] = dfs(x + 1, x + 2); 
		return f[hsh];
	}
	if(b[x] > (n - y + 1) * 3) return 0;
	int ret = 0;
	if(b[x] >= 3){
		b[x] -= 3;
		(ret += dfs(x, y + 1)) %= mod;
		b[x] += 3;
	}
	if(b[y] >= 3){
		b[y] -= 3;
		(ret += dfs(x, y + 1)) %= mod;
		b[y] += 3;
	}
	if(b[x] >= 1 && b[y] >= 1){
		b[x]--, b[y]--;
		(ret += dfs(x, y + 1)) %= mod;
		b[x]++, b[y]++;
	}
	return ret;
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> b[i];
	cout << dfs(1, 2);
	return 0;
}

P3322 [SDOI2015] 排序

假设我们总是先做小区间的操作,再做大区间的操作。我们发现,一次交换长度为 \(2^i\) 的操作最多使得 2 段长度为 \(2^{i + 1}\) 的区间恢复有序。这里恢复有序是指序列的开头是 \(1 + k \times 2 ^ {i + 1}\) 并且后面的所有都是连续递增的。由于我们总是先做小区间,再做大区间,因此在做大区间时必须保证小区间内部先是有序的。如果我们发现一个长度为 \(2^{i + 1}\) 的区间并不有序,那么我们就要尝试做 \(2^i\) 之间的交换操作使他变有序。具体来说,如果发现全部有序,就不做这次操作;如果发现一段不有序,就交换前后;如果发现两段不有序,就尝试交换前面的前半部分/后半部分与后面的前半部分/后半部分(总共4种),验证是否合法;如果有更多的不有序段,肯定就不行了。
进一步考虑,如果说从小到大做可以,那么随意交换操作之间的顺序也是可以的。相当于我们开了上帝视角,先做大区间,然后再交换对应的小区间也能使得大区间变得有序。因此如果说有一种从小到大做的方案做了 \(k\) 次操作,那么会像答案贡献 \(k!\)

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 15;
int a[1 << N], n, fac[N], ans;
bool check(int k, int i){
	if(a[i] % (1 << k) != 0) return 0;
	for(int j = i + 1; j <= i + (1 << k) - 1; ++j){
		if(a[j] != a[j - 1] + 1) return 0;
	}
	return 1;
}
void Swap(int len, int i, int j){
	for(int k = 0; k < (1 << len); ++k) swap(a[i + k], a[j + k]); 
}
void dfs(int x, int step){
	for(int i = 0; i + (1 << x) - 1 < (1 << n); i += (1 << x)){
		if(!check(x, i)) return;
	}
	if(x == n){
		ans += fac[step];
		return;
	}
	int cnt = 0, nxt = x + 1, tmp[4];
	for(int i = 0; i + (1 << nxt) - 1 < (1 << n); i += (1 << nxt)){
		if(!check(nxt, i)){
			if(cnt == 4) return;
			tmp[cnt++] = i, tmp[cnt++] = i + (1 << x);
		}
	}
	if(cnt == 0) return dfs(x + 1, step);
	for(int i = 0; i < cnt; ++i){
		for(int j = i + 1; j < cnt; ++j){
			Swap(x, tmp[i], tmp[j]);
			dfs(x + 1, step + 1);
			Swap(x, tmp[i], tmp[j]);
		}
	}
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n;
	fac[0] = 1;
	for(int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i; 
	for(int i = 0; i < (1 << n); ++i) cin >> a[i], a[i]--;
	dfs(0, 0);
	cout << ans << '\n'; 
	return 0;
}

P6765 [APIO2020] 交换城市

灵活的 Kruskal 重构树。首先要对题意做一步转化,\(x \to y\) 能够互相交换,当且仅当他们所在的联通块不是一条链。
那么我们还是按边权从小到大枚举每条边,考虑加入这条边之后。如果发现所在的联通块从链变成了非链(环/有度数大于等于3的点),那么就建一个新点,向联通块所有点连树边,并用新点代表联通块(这一步就是 Kruskal)。
环是好判断的(就看是不是已经被联通),维护是否有大于 3 的点就维护链的端点,看看拼链的时候是不是端点相连,如果不是,就说明有度数 \(\ge 3\) 的点。
注意,如果当连一条边时,如果发现两个联通块之中有一个已经是非链了,那么合并后的联通块也肯定不是非链,直接开新点相连即可。
由于有合并两个联通块的操作,所以要启发式合并。
给我们的启发是,维护的不是点之间的瓶颈路,而是维护类似只经过 \(\le w\) 的边能否到达一个环/非链/某些关键点时,也可以 kruskal 重构树。
怎么用交互库也是一个可以学一下的东西。

Code
#include "swap.h"
#include <bits/stdc++.h>
using namespace std;
const int V = 2e5 + 5;
int f[V], n, m, tot, a[V], ed[V], p[V][2], st[V][21], dep[V];
bitset<V> is;
vector<int> tr[V], s[V];
struct edge{
	int u, v, w;
	bool operator < (const edge &b) const{
		return w < b.w;
	}
}e[V * 2]; 
int getf(int u){ return (u == f[u] ? u : f[u] = getf(f[u])); }
void dfs(int u){
	for(int i = 1; (1 << i) <= dep[u]; ++i) st[u][i] = st[st[u][i - 1]][i - 1];
	for(int v : tr[u]){
		dep[v] = dep[u] + 1;
		st[v][0] = u;
		dfs(v);
	}
}
int lca(int u, int v){
	if(dep[u] < dep[v]) swap(u, v);
	for(int i = 19; i >= 0; --i){
		if(dep[st[u][i]] >= dep[v]) u = st[u][i];
	}
	if(u == v) return u;
	for(int i = 19; i >= 0; --i){
		if(st[u][i] != st[v][i]) u = st[u][i], v = st[v][i];
	}
	return st[u][0];
}
void init(int N, int M,
          vector<int> U, vector<int> V, vector<int> W) {
	n = N, m = M, tot = n + 1;
	is.reset();
	for(int i = 1; i <= n; ++i) 
		f[i] = i, ed[i] = 0, p[i][0] = p[i][1] = i, s[i].emplace_back(i);
	for(int i = 0; i < m; ++i){
		int u = U[i], v = V[i], w = W[i];
		++u, ++v;
		e[i + 1] = {u, v, w};
	}
	sort(e + 1, e + 1 + m);
	for(int i = 1; i <= m; ++i){
		int u = e[i].u, v = e[i].v, w = e[i].w;
		int fu = getf(u), fv = getf(v);
		auto merge = [](int x, int u){
			tr[x].insert(tr[x].end(), s[u].begin(), s[u].end());	
			s[u].clear();
		};
		if(fu == fv){
			if(is[fu]) continue;
			is[fu] = 1;
			a[++tot] = w;
			merge(tot, fu);
			s[fu].emplace_back(tot);
		}
		else{
			if(is[fu] || is[fv]){
				a[++tot] = w;
				f[fv] = fu;
				is[fu] = 1;
				merge(tot, fu), merge(tot, fv);
				s[fu].emplace_back(tot);
			}
			else{
				if(ed[u] < 2 && ed[v] < 2){
					if(s[fu].size() < s[fv].size()) swap(fu, fv), swap(u, v);
					s[fu].insert(s[fu].end(), s[fv].begin(), s[fv].end());
					s[fv].clear();
					f[fv] = fu;
					int x = p[fu][ed[u] ^ 1], y = p[fv][ed[v] ^ 1];
					ed[u] = ed[v] = 2;
					ed[x] = 0, ed[y] = 1;
					p[fu][0] = x, p[fu][1] = y;
				}
				else{
					f[fv] = fu;
					is[fu] = 1;
					a[++tot] = w;
					merge(tot, fu);
					merge(tot, fv);
					s[fu].emplace_back(tot);
				}
			}
		}
	}
	dep[tot] = 1;
	dfs(tot);
}
int getMinimumFuelCapacity(int X, int Y) {
	++X, ++Y; 
	int L = lca(X, Y);
	if(L == 0) return -1;
	return a[L];
}

P2898 [USACO08JAN] Haybale Guessing G

考虑一个经典问题,有 \(q\) 次操作,每次是对 \([l, r]\) 进行区间染色 \(c\),求最终的序列。
线段树/ODT什么的就不说了,说一个优雅的并查集做法。
我们将染色倒过来做,也就是说一段区间一旦染色,后面的操作都得跳过它。考虑维护每个点向右第一个没有染色的点 \(f_i\),初始时 \(f_i = i\)
当我们对区间 \([l, r]\) 进行染色时,维护一个指针 \(p\),一开始 \(p \gets f_l\),然后不断跳 \(p \gets f_p\),直到跳出区间。那么我们遍历到的所有 \(p\) 就是这个区间内还没有染色的位置,把他们染上对应的颜色。同时,对于跳到的每一个 \(p\),更新他们的 \(f_p \gets \operatorname{getf}(p + 1)\)。这样就做完了。
本题来说,二分答案之后,按权值从小到大做区间染色。最后验证是否可行,就是看每种最小值的区间的交集的最小值是否能取到,有一些细节操作区间并集/交集看代码吧。
代码写的是线段树;如果用上面的办法维护,我们就得按从大到小进行排序,查询一段区间能否取到就是看这段中还有没有没染的位置就行。

Code
#include <bits/stdc++.h>
using namespace std;
typedef tuple<int, int, int> tpi;
const int Q = 2.5e4 + 5, N = 1e6 + 5;
int l[Q], r[Q], x[Q], V[Q], tot, n, q, pl[Q], pr[Q], opl[Q], opr[Q];
struct Segment{
	int tr[N << 2], tag[N << 2];
	#define ls(p) p << 1
	#define rs(p) p << 1 | 1
	void clear(){ memset(tr, 0x3f, sizeof(tr)); memset(tag, 0, sizeof(tag)); }
	void addtag(int p, int k){
		tag[p] = k, tr[p] = k;
	}
	void pushdown(int p){
		if(tag[p]){
			addtag(ls(p), tag[p]);
			addtag(rs(p), tag[p]);
			tag[p] = 0;
		}
	}
	void pushup(int p){
		tr[p] = min(tr[ls(p)], tr[rs(p)]);
	}
	void update(int L, int R, int k, int p = 1, int pl = 1, int pr = n){
		if(L <= pl && R >= pr) return addtag(p, k);
		int mid = (pl + pr) >> 1;
		pushdown(p);
		if(L <= mid) update(L, R, k, ls(p), pl, mid);
		if(R > mid) update(L, R, k, rs(p), mid + 1, pr);
		pushup(p);
	}
	int query(int L, int R, int p = 1, int pl = 1, int pr = n){
		if(L <= pl && R >= pr) return tr[p];
		int mid = (pl + pr) >> 1, ret = 1e9;
		pushdown(p);
		if(L <= mid) ret = query(L, R, ls(p), pl, mid);
		if(R > mid) ret = min(ret, query(L, R, rs(p), mid + 1, pr));
		return ret;
	}
}tr;
bool merge(int &l, int &r, int L, int R, int op){
	if(!l) return l = L, r = R, 1;
	if(r < L || l > R) return 0;
	if(op == 0) l = max(l, L), r = min(r, R);
	if(op == 1) l = min(l, L), r = max(r, R);
	return 1;
}
bool check(int k){
	memset(pl, 0, sizeof(pl));
	memset(pr, 0, sizeof(pr));
	memset(opl, 0, sizeof(opl));
	memset(opr, 0, sizeof(opr));
	tr.clear();
	set<int> s;
	for(int i = 1; i <= k; ++i){
		if(!merge(pl[x[i]], pr[x[i]], l[i], r[i], 0)) return 0;
		merge(opl[x[i]], opr[x[i]], l[i], r[i], 1);
		s.emplace(x[i]);
	}
	for(int i : s) tr.update(opl[i], opr[i], i);
	for(int i : s){
		if(tr.query(pl[i], pr[i]) > i) return 0;
	}
	return 1;
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n >> q;
	for(int i = 1; i <= q; ++i){
		cin >> l[i] >> r[i] >> x[i];
		V[++tot] = x[i];
	}
	sort(V + 1, V + 1 + tot);
	tot = unique(V + 1, V + 1 + tot) - V - 1;
	for(int i = 1; i <= q; ++i) x[i] = lower_bound(V + 1, V + 1 + tot, x[i]) - V;
	int l = 1, r = q;
	while(l < r){
		int mid = (l + r + 1) >> 1;
		if(check(mid)) l = mid;
		else r = mid - 1;
	}
	cout << (l == q ? 0 : l + 1);
	return 0;
}

P10806 [CEOI 2024] 洒水器

一道典题的严格弱化版。
加强版题题意:给定 \(n\) 的点,第 \(i\) 个的点有权 \(p_i\)。对于每个点,我们可以选择其向左覆盖 \([i - p_i, i - 1]\) 还是向右覆盖 \([i + 1, i + p_i]\) 的一种。求问是否能将所有点覆盖,并输出方案。
这种覆盖问题可以考虑设计状态 \(f_i\) 表示当只有前 \(i\) 个点时,能覆盖到的最大前缀是 \([1, f_i]\)。这样设计的好处是,我们给限定较少的可行性 dp 转化成了有更多信息的最优性 dp,这样 dp 值可以帮助我们转移,并且最后判断是否可行也是简单的,只需要看 \(f_n \ge n\) 即可。并且 \(f_i\) 自带单调性,这也就带来了下文中决策时的单调性。
如果第 \(i\) 个点向右覆盖,要么 \(f_i \gets f_{i - 1}\),或者 \(f_i \gets p_i + i \text{ if } f_{i - 1} \ge i\)
如果第 \(i\) 个点向左覆盖,那么我们先二分找到最小的 \(t, s.t. f_t \ge i - p_i - 1\),如果找不到就跟向右覆盖直接 \(f_i \gets f_{i - 1}\) 一样了。否则如果有 \(t\),也就是说我们向左覆盖可以跟前面的一个点的最长前缀拼上,并且中间的点往左覆盖就都没有意义了,让中间的点向右覆盖肯定更优秀,所以 \(f_i \gets \max(i - 1, \max_{j = t + 1}^{i - 1} p_j + j)\)。这个 ST 表优化一下。
输出方案理解了转移之后是 trivial 的,然后就做完了。

对于本题,容易想到二分答案 \(k\)。然后就发现验证可行性的部分不是跟上面这道题一模一样吗,甚至这道题所有的覆盖长度都是一样的 \(k\)。因此还是套用上面的定义,\(f_i\) 表示只用前 \(i\) 个洒水器能覆盖到花的最长前缀,只不过这次向左覆盖那一部分的转移可以化简很多,不需要 DS 优化了。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
bool dir[N];
int f[N], s[N], pre[N], p[N], n, m;
char ans[N];
int find(int x){ return upper_bound(p + 1, p + 1 + m, x) - p - 1; }
bool check(int k){
	f[0] = 0;
	for(int i = 1; i <= n; ++i){
		f[i] = f[i - 1], pre[i] = i - 1, dir[i] = 1;
		if(f[i - 1] >= find(s[i] - 1)) f[i] = find(s[i] + k);
		int pos = find(s[i] - k - 1);
		if(i >= 2 && f[i - 2] >= pos){
			int tmp = max(find(s[i - 1] + k), find(s[i]));
			if(tmp > f[i]) f[i] = tmp, pre[i] = i - 2, dir[i] = 0;
		}
		else if(f[i - 1] >= pos){
			int tmp = find(s[i]);
			if(tmp > f[i]) f[i] = tmp, pre[i] = i - 1, dir[i] = 0;
		}
	}
	return f[n] == m;
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n >> m;
	for(int i = 1; i <= n; ++i) cin >> s[i];
	for(int i = 1; i <= m; ++i) cin >> p[i];
	int l = 0, r = 1e9;
	while(l < r){
		int mid = (l + r) >> 1;
		if(check(mid)) r = mid;
		else l = mid + 1;
	} 
	if(check(l)){
		cout << l << '\n';
		for(int i = n; i; i = pre[i]){
			for(int j = pre[i] + 1; j < i; ++j) ans[j] = 'R';
			ans[i] = (dir[i] ? 'R' : 'L');
		}
		for(int i = 1; i <= n; ++i) cout << ans[i];
	}
	else cout << -1;
	return 0;
}

P3147 [USACO16OPEN] 262144 P

一个典题。定义状态 \(f_{i,j}\) 表示从 \(i\) 节点向右合并出 \(j\) 的右端点,如果没有为 0。那么转移是 \(f_{i, j} = f_{f_{i, j - 1}, j - 1}\)。初始值一开始赋为 \(f_{i, a_i} = i + 1\)。再解释一下上面的状态,就是合并出这个 \(j\) 的右端点对于 \(i\) 来说是唯一的,因为我们一开始的初值显然是唯一的,每次合并又一定使得区间长度增加,所以是唯一的。
这个东西还有一些性质。注意下面的状态定义和上面略有不同,是 \(f_{i, j}\) 表示 \(i\) 节点向左合并出 \(j\) 的左端点。首先对于一个右端点 \(i\) 来说,它能向左合并出的值一定是一段区间 \([a_i, R]\),这是显然的。同时,一个能合并成一个数的区间个数是 \(O(n \log n)\)。假设我们令 \(ver(j):= \{i\}\) 表示能向左合成出 \(j\) 的那些右端点的集合。就是向上合并,由于区间长度随层数递增,第 \(j\) 层的 \(ver\) 会比上一层的少最前面的几个数,并且加入几个 \(a_x = j\) 的数。发现当所有数全部相同的时候 \(\sum |ver(j)|\) 取到上界。
会了这些之后,你就可以把值域加强到 \(10^9\) 了,也就是这道加强版。按 \((j, i)\) 维护一个优先队列,先合并小的,再合并大的(实际上就是维护 \(ver(j)\),并向上转移)。同时由于对于每个端点 \(i\) 来说,合并的值是一段区间,所以开一个 vector 存上面的 \(f\) 就行。由于用了优先队列,复杂度和能合并成 1 的个数成正比,所以是两个 log,不过实现精细应该可以做到单 log 的。
求出所有能合并成一个的区间之后,加强版最后还套了个 dp,不过这是 trivial 的。下面是加强版的代码。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
typedef pair<int, int> pii; 
vector<int> f[N];
priority_queue<pii, vector<pii>, greater<pii> > q;
int n, a[N], dp[N];
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i], q.emplace(a[i], i), f[i].emplace_back(i - 1);
	while(!q.empty()){
		int i, j; tie(j, i) = q.top(); q.pop();
		int pos = f[i].at(j - a[i]);
		if(j >= a[pos] && j < a[pos] + f[pos].size()){
			int k = f[pos].at(j - a[pos]);
			f[i].emplace_back(k);
			q.emplace(j + 1, i);
		}
	}
	memset(dp, 0x3f, sizeof(dp));
	dp[0] = 0;
	for(int i = 1; i <= n; ++i){
		for(int j : f[i]){
			dp[i] = min(dp[i], dp[j] + 1);
		}
	}
	cout << dp[n];
	return 0;
}

P3076 [USACO13FEB] Taxi G

丁香之路的严格弱化版。题意几乎一样,只不过本题中你可以拉一头牛拉到一半然后把它丢下去,去拉另一头。
跟丁香之路一样,运送的路径是一条以 0 开始 \(m\) 结束的欧拉路径,还是先加一条 \(m \to 0\) 变成欧拉回路。然后你发现,这个拉一半可以把一头牛丢下去就等价于丁香之路里的 \(a_i \to a_i + 1 \to \cdots \to b_i(a_i < b_i)\) 的建边,大于也一样。
由于连上中间的点不改变其出度与入度之差,也就不影响答案,那我们实际上就只用对起点、终点统计度数配对就行。加边的过程就是入度出度匹配的过程,类似排序不等式,是比较 trivial 的。
实现来看,就是丁香之路去掉了最后一步使原图变为联通的部分。

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
map<int, int> out, in;
set<int> p;
priority_queue<int> q[2];
int n, m, ans; 
signed main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n >> m;
	for(int i = 1; i <= n; ++i){
		int a, b;
		cin >> a >> b;
		ans += abs(b - a);
		out[a]++, in[b]++;
		p.emplace(a), p.emplace(b);
	}
	out[m]++, in[0]++;
	p.emplace(m), p.emplace(0);
	for(int x : p){
		int tmp = in[x] - out[x];
		if(tmp < 0){
			for(int i = 1; i <= -tmp; ++i){
				q[0].emplace(x);
			}
		}
		else{
			for(int i = 1; i <= tmp; ++i){
				q[1].emplace(x);
			}
		}
	}
	while(!q[0].empty()) ans += abs(q[0].top() - q[1].top()), q[0].pop(), q[1].pop();
	cout << ans;
	return 0;
}

P3243 [HNOI2015] 菜肴制作

没见过的一个经典结论。题目中要求的“最小”排列,就是反图上字典序最大的拓扑。(注意这个结论的证明有赖于 DAG 的性质,任意一个排列集合中是没有这个性质的。)
实际上与其说这个是字典序最大,不如换一个更加好理解的说法。假设我们想取到 1 所在的位置最靠前,那么我们将原图分为两部分,一部分是必须在 1 之前先被解锁的点 \(x\)(存在从 \(x\) 到 1 的路径的点),其余的是 \(y\)。我们最优的方法,就是先把所有 \(x\) 解锁了,然后解锁 1,然后再去看 \(y\)。那么在反序列上,我们就得先把所有 \(y\) 填了,然后填 1,然后填 \(x\)。也就是说,在反图上拓扑时,不到必须填 1 的时候绝不填 1。对于其余位置同理。也就是说,我们每次填的都是字典序最大的那个数(这是在不得不填的情况下的最优办法,填了别的肯定更劣)。把这个写成归纳法,就是 ppip 的那个证明了。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int deg[N], n, m;
vector<int> ans, e[N];
void solve(){
	cin >> n >> m;
	memset(e, 0, sizeof(e));
	memset(deg, 0, sizeof(deg));
	ans.clear();
	for(int i = 1; i <= m; ++i){
		int u, v; cin >> u >> v;
		e[v].emplace_back(u);
		deg[u]++;
	}
	priority_queue<int> q; 
	for(int i = 1; i <= n; ++i){
		if(!deg[i]) q.push(i);
	}
	while(!q.empty()){
		int u = q.top(); q.pop();
		ans.emplace_back(u);
		for(int v : e[u]){
			deg[v]--;
			if(!deg[v]) q.push(v);
		}
	}
	if(ans.size() < n) return cout << "Impossible!", void();
	reverse(ans.begin(), ans.end());
	for(int x : ans) cout << x << ' ';
}
int main(){
	int T; cin >> T;
	while(T--) solve(), cout << '\n';
	return 0;
}

P10804 [CEOI 2024] 玩具谜题

严格弱于这个的一道题。思路都很像。本题也是把那个交点找出来,然后看一下上下左右能到的最远的地方,转移相邻就行。单次判断用了无脑的 \(O(\frac{n}{w})\),不过能过就是了。
注意 bitset & 是要花额外空间的,在这题这么写会 MLE,改成 &= 就好了

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1505;
char mp[N][N];
int n, m, k, l, xh, yh, xv, yv, sx, sy, ex, ey; // k hor, l ver
bitset<N> f[N], g[N], vis[N], limx[N], limy[N]; // hor:f[i][j], ver:g[j][i]  
void dfs(int x, int y){
	if(x == ex && y == ey){
		cout << "YES";
		exit(0); 
	}
	vis[x][y] = 1;
	for(int kx : {-1, 1}){
		int xx = x + kx;
		if(xx >= 1 && xx <= n){
            f[0] = limy[y];
            f[0] &= f[xx];
            f[0] &= f[x];
			if(!vis[xx][y] && mp[xx][y] != 'X' && f[0].count())
				dfs(xx, y);
		}
	}
	for(int ky : {-1, 1}){
		int yy = y + ky;
		if(yy >= 1 && yy <= m){
            f[0] = limx[x];
            f[0] &= g[yy];
            f[0] &= g[y];
			if(!vis[x][yy] && mp[x][yy] != 'X' && f[0].count())
				dfs(x, yy);
		}
	}
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> m >> n >> k >> l;
	cin >> yh >> xh >> yv >> xv;
	++yh, ++xh, ++yv, ++xv; 
	sx = xh, sy = yv;
	for(int i = 1; i <= n; ++i){
        for(int j = max(1, i - l + 1); j <= i; ++j)
            limx[i][j] = 1;
    }
    for(int j = 1; j <= m; ++j){
        for(int i = max(1, j - k + 1); i <= j; ++i)
            limy[j][i] = 1;
    }
	for(int i = 1; i <= n; ++i){
		for(int j = 1; j <= m; ++j){
			cin >> mp[i][j];
			if(mp[i][j] == '*') ex = i, ey = j;
		}
	}
	// f
	for(int i = 1; i <= n; ++i){
		int lst = m + 1;
		for(int j = m; j >= 1; --j){
			if(mp[i][j] == 'X') lst = j;
			f[i][j] = (lst >= j + k);
		}
	}
	// g 
	for(int j = 1; j <= m; ++j){
		int lst = n + 1;
		for(int i = n; i >= 1; --i){
			if(mp[i][j] == 'X') lst = i;
			g[j][i] = (lst >= i + l);
		}
	}
	dfs(sx, sy);
	cout << "NO";
	return 0;
}

P2115 [USACO14MAR] Sabotage G

分数规划板子,别忘了。比如求的是最小值,那就二分 \(mid\),验证原式是否能 \(\le mid\)

Code
#include <bits/stdc++.h>
using namespace std;
typedef long double ldb;
const int N = 1e5 + 5;
const ldb eps = 1e-5;
ldb a[N], m[N], f[N], sum;
int n;
bool check(ldb k){
	ldb ans = 1e9;
	for(int i = 2; i <= n - 1; ++i){
		a[i] = k - m[i];
		f[i] = min(f[i - 1] + a[i], a[i]);
		ans = min(ans, f[i]);
	}
	return ans <= k * n - sum;
}
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> m[i], sum += m[i];
	ldb l = 0, r = 1e4;
	while(fabs(r - l) > eps){
		ldb mid = (l + r) / 2.;
		if(check(mid)) r = mid;
		else l = mid;
	}
	cout << fixed << setprecision(3) << l; 
	return 0;
}

P10217 [省选联考 2024] 季风

推式子题,别忘了绝对值是可以暴力分讨去掉的。

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5, inf = 4e18;
struct node{
	int pl, pr; 
	node (){ pl = 0, pr = inf; }
};
int n, k, x, y, sumx[N], sumy[N], sx, sy;
void chmin(int &x, int y){ x = min(x, y); }
void chmax(int &x, int y){ x = max(x, y); }
void get(node &x, int k, int b, int op){ // op = 0 k * p <= b, op = 1 k * p >= b
	if(k == 0){
		if((op == 0 && b < 0) || (op == 1 && b > 0)) x.pr = -1;
		return;
	}   
	if((k < 0 && op == 0) || (k > 0 && op == 1)) chmax(x.pl, ceil(1. * b / k));
	else chmin(x.pr, floor(1. * b / k));
}
void solve(){
	cin >> n >> k >> x >> y;
	for(int i = 1; i <= n; ++i){
		int a, b; cin >> a >> b;
		sumx[i] = a + sumx[i - 1];
		sumy[i] = b + sumy[i - 1];
	}
	sx = sumx[n], sy = sumy[n];
	int m = inf;
	for(int q = 0; q < n; ++q){
		for(int i : {0, 1}){
			for(int j : {0, 1}){
				node p;
				get(p, sx, x - sumx[q], i);
			 	get(p, sy, y - sumy[q], j);
				if(i == 1 && j == 1) get(p, sx + sy - n * k, q * k + x + y - sumx[q] - sumy[q], 0);
				else if(i == 0 && j == 1) get(p, - sx + sy - n * k, q * k + y - x + sumx[q] - sumy[q], 0);
				else if(i == 1 && j == 0) get(p, sx - sy - n * k, q * k + x - y + sumy[q] - sumx[q], 0);
				else get(p, - sx - sy - n * k, q * k - x - y + sumx[q] + sumy[q], 0);
				if(p.pl <= p.pr) chmin(m, p.pl * n + q);
			}
		}
	}
	cout << (m == inf ? -1 : m) << '\n';
}
signed main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	int T; cin >> T;
	while(T--) solve(); 
	return 0;
}

P4375 [USACO18OPEN] Out of Sorts G

排序还是没有好好学。先看正常的冒泡排序,那么最少几次能让原序列变成有序呢?答案是每个位置前面比它大的数的个数取 \(\max\)。因为执行完这么多次之后,对于每个位置,前面的数都比它小,后面的数都比它大。
那对于本题这个冒泡排序的变种呢?我们先做离散化,对于相同的数认为前面的数比后面的数小。这样变成一个 \(n\) 的排列之后,答案就是 \(\max_i{\sum_{1 \le k \le i} [a_k > i]}\)。这是因为我们每次操作会把一个不是 \([1, i]\) 的丢到后面,把一个应该是 \([1, i]\) 的挪进来。做完这么多次之后,所有前缀都是 \([1,i]\) 本身的数了。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, a[N];
map<int, vector<int> > buc; 
struct BIT{
	#define lowbit(x) (x & (-x))
	int tr[N];
	void add(int x, int k){
		for(int i = x; i <= n; i += lowbit(i))
			tr[i] += k;
	}
	int qry(int x){
		int res = 0;
		for(int i = x; i; i -= lowbit(i))
			res += tr[i];
		return res;
	}
}tr;
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin >> n;
	for(int i = 1; i <= n; ++i){
		cin >> a[i];
		buc[a[i]].emplace_back(i);
	}
	int cnt = 0;
	for(auto t : buc){
		vector<int> v;
		tie(ignore, v) = t;
		for(int x : v) a[x] = ++cnt;
	}
	int ans = 1;
	for(int i = 1; i <= n; ++i){
		tr.add(a[i], 1);
		ans = max(ans, i - tr.qry(i));
	}
	cout << ans;
	return 0;
}
posted @ 2025-10-21 22:30  Hengsber  阅读(9)  评论(0)    收藏  举报