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;
}

浙公网安备 33010602011771号