贪心
1. 模拟费用流贪心(可撤销贪心)
一个非常玄妙的算法。
*I. CF280D k-Maximum Subsequence Sum
一道用数据结构维护的模拟费用流贪心。首先当
这个问题一脸可以费用流的样子:建立
显然直接流是不可接受的,考虑模拟费用流:注意到每次从
const int N = 1e5 + 5;
struct data {
int sum, pre, suf, ans, prep, sufp, ansl, ansr;
} I, pos[N << 2], neg[N << 2];
data operator + (data x, data y) {
data z = I; z.sum = x.sum + y.sum;
if(x.pre > z.pre) z.pre = x.pre, z.prep = x.prep;
if(x.sum + y.pre > z.pre) z.pre = x.sum + y.pre, z.prep = y.prep;
if(y.suf > z.suf) z.suf = y.suf, z.sufp = y.sufp;
if(y.sum + x.suf > z.suf) z.suf = y.sum + x.suf, z.sufp = x.sufp;
if(x.ans > z.ans) z.ans = x.ans, z.ansl = x.ansl, z.ansr = x.ansr;
if(y.ans > z.ans) z.ans = y.ans, z.ansl = y.ansl, z.ansr = y.ansr;
if(x.suf + y.pre > z.ans) z.ans = x.suf + y.pre, z.ansl = x.sufp, z.ansr = y.prep;
return z;
}
int n, m, a[N], rev[N << 2];
void init(int x, int p) {
int pp = max(0, a[p]), po = a[p] < 0 ? -1 : p;
pos[x] = {a[p], pp, pp, pp, po, po, po, po};
int np = max(0, -a[p]), no = a[p] > 0 ? -1 : p;
neg[x] = {-a[p], np, np, np, no, no, no, no};
}
void push(int x) {
pos[x] = pos[x << 1] + pos[x << 1 | 1];
neg[x] = neg[x << 1] + neg[x << 1 | 1];
}
void swp(int x) {rev[x] ^= 1, swap(pos[x], neg[x]);}
void down(int x) {if(rev[x]) swp(x << 1), swp(x << 1 | 1), rev[x] = 0;}
void build(int l, int r, int x) {
if(l == r) return init(x, l);
int m = l + r >> 1;
build(l, m, x << 1), build(m + 1, r, x << 1 | 1), push(x);
}
void update(int l, int r, int p, int x) {
if(l == r) return init(x, p);
int m = l + r >> 1; down(x);
if(p <= m) update(l, m, p, x << 1);
else update(m + 1, r, p, x << 1 | 1);
push(x);
}
void modify(int l, int r, int ql, int qr, int x) {
if(ql <= l && r <= qr) return swp(x);
int m = l + r >> 1; down(x);
if(ql <= m) modify(l, m, ql, qr, x << 1);
if(m < qr) modify(m + 1, r, ql, qr, x << 1 | 1);
push(x);
}
data query(int l, int r, int ql, int qr, int x) {
if(ql <= l && r <= qr) return pos[x];
int m = l + r >> 1; data ans = I; down(x);
if(ql <= m) ans = query(l, m, ql, qr, x << 1);
if(m < qr) ans = ans + query(m + 1, r, ql, qr, x << 1 | 1);
return ans;
}
int main(){
cin >> n, I = {0, 0, 0, 0, -1, -1, -1, -1};
for(int i = 1; i <= n; i++) a[i] = read();
build(1, n, 1), m = read();
for(int i = 1, l, r, k; i <= m; i++) {
if(!read()) l = read(), a[l] = read(), update(1, n, l, 1);
else {
l = read(), r = read(), k = read();
int ans = 0; vpii oper;
while(k--) {
data res = query(1, n, l, r, 1);
if(res.ansl == -1) break;
ans += res.ans, oper.pb(res.ansl, res.ansr);
modify(1, n, res.ansl, res.ansr, 1);
} print(ans), pc('\n');
for(pii it : oper) modify(1, n, it.fi, it.se, 1);
}
}
return flush(), 0;
}
*II. P3620 [APIO/CTSC 2007] 数据备份
一个初步想法是每次选代价最小的连,但这样不一定最优,因为与它相邻的两段就不能被选,可能导致我们因为
上述结论让这个贪心策略的反悔变得容易:如果选择了
模拟费用流:注意到每条边必然连接相邻的两个点,因此将所有点按照编号奇偶性分类:从超级源点
const int N = 1e5 + 5;
const int inf = 2e9;
int n, k, ans, s[N], d[N], pre[N], suf[N];
priority_queue <pii, vector <pii>, greater <pii>> q;
int main() {
fprintf(stderr, "%.3lf\n", (&Med - &Mbe) / 1048576.0);
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> s[i], d[i] = s[i] - s[i - 1];
for(int i = 2; i <= n; i++) q.push({d[i], i}), pre[i] = i - 1, suf[i] = i + 1;
d[1] = 1e9, d[n + 1] = 1e9;
while(k--) {
while(q.top().fi != d[q.top().se]) q.pop();
pii t = q.top(); int p = t.se, nw = 0; q.pop(), ans += t.fi;
nw += d[pre[p]], d[pre[p]] = inf, pre[p] = pre[pre[p]], suf[pre[p]] = p;
nw += d[suf[p]], d[suf[p]] = inf, suf[p] = suf[suf[p]], pre[suf[p]] = p;
d[p] = t.fi = nw - t.fi, q.push(t);
}
cout << ans << endl;
return flush(), 0;
}
2. 数据结构维护贪心
I. P3545 [POI2012]HUR-Warehouse Store
首先忽略所有顾客,计算出每天结束时拥有的货物数量即
另一种解法:从左往右考虑每个顾客
两种解法的时间复杂度均为
const int N = 3e5 + 5;
ll n, cur, a[N], b[N], id[N];
vint ans;
ll val[N << 2], laz[N << 2];
void build(int l, int r, int x) {
if(l == r) return val[x] = a[l], void();
int m = l + r >> 1;
build(l, m, x << 1), build(m + 1, r, x << 1 | 1);
val[x] = min(val[x << 1], val[x << 1 | 1]);
}
void tag(int x, ll v) {laz[x] += v, val[x] += v;}
void push(int x) {if(laz[x]) tag(x << 1, laz[x]), tag(x << 1 | 1, laz[x]), laz[x] = 0;}
void modify(int l, int r, int ql, int qr, int x, int v) {
if(ql <= l && r <= qr) return tag(x, v), void();
int m = l + r >> 1; push(x);
if(ql <= m) modify(l, m, ql, qr, x << 1, v);
if(m < qr) modify(m + 1, r, ql, qr, x << 1 | 1, v);
val[x] = min(val[x << 1], val[x << 1 | 1]);
}
ll query(int l, int r, int ql, int qr, int x) {
if(ql <= l && r <= qr) return val[x];
ll m = l + r >> 1, ans = 1e18; push(x);
if(ql <= m) ans = query(l, m, ql, qr, x << 1);
if(m < qr) cmin(ans, query(m + 1, r, ql, qr, x << 1 | 1));
return ans;
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) a[i] = read() + a[i - 1];
for(int i = 1; i <= n; i++) b[i] = read(), id[i] = i;
sort(id + 1, id + n + 1, [&](int u, int v) {return b[u] < b[v];});
build(1, n, 1);
for(int i = 1, p = id[1]; i <= n; p = id[++i])
if(query(1, n, p, n, 1) >= b[p])
modify(1, n, p, n, 1, -b[p]), ans.pb(p);
cout << ans.size() << endl, sor(ans);
for(int it : ans) cout << it << " "; cout << endl;
return flush(), 0;
}
*II. [BZOJ3441]乌鸦喝水
神仙题。首先对问题进行初步分析:乌鸦肯定是能喝就喝,因为留到后面再喝不会让局面变得更优(贪心思想所在)。因此本题变成了一道萌萌模拟题,但
接下来的部分看了题解:设
设当前答案为
说简单点,就是我们通过关注当前存活的水罐中
const int N = 1e5 + 5;
int n, m, ans, rd = 1, lg, a[N], w[N], c[N], x;
int add(int x, int v) {while(x <= n) c[x] += v, x += x & -x;}
int query(int x) {int s = 0; while(x) s += c[x], x -= x & -x; return s;}
int query(int l, int r) {return query(r) - query(l - 1);}
int find(int val) {
int p = 0, s = 0;
for(int i = lg; ~i; i--) {
int np = p + (1 << i);
if(np > n) continue;
if(s + c[np] <= val) s += c[np], p = np;
} return p;
}
pii d[N];
int main(){
cin >> n >> m >> x, lg = log2(n);
for(int i = 1; i <= n; i++) w[i] = read(), add(i, 1);
for(int i = 1; i <= n; i++) a[i] = read(), d[i] = {max(0ll, (x - w[i]) / a[i] + 1), i};
sort(d + 1, d + n + 1);
for(int i = 1, las = 1; i <= n; i++) {
int dd = d[i].fi - d[i - 1].fi;
if(dd == 0) {add(d[i].se, -1); continue;}
while(rd <= m && dd) {
int res = query(las, n);
if(res < dd) rd++, las = 1, ans += res, dd -= res;
else las = find(query(las - 1) + dd) + 1, ans += dd, dd = 0;
} add(d[i].se, -1);
} cout << ans << endl;
return flush(), 0;
}
III. P4098 [HEOI2013]ALO
考虑枚举作为区间第二大值的元素
int n, ans, node, a[N], R[N], son[N << 5][2], val[N << 5];
void modify(int pre, int &x, int v, int bit) {
val[x = ++node] = val[pre] + 1, cpy(son[x], son[pre], 2);
if(bit == -1) return;
int c = v >> bit & 1;
modify(son[pre][c], son[x][c], v, bit - 1);
}
int query(int v, int bit, int x, int y) {
if(bit == -1) return 0;
int c = v >> bit & 1;
if(val[son[y][c ^ 1]] - val[son[x][c ^ 1]]) return (1 << bit) + query(v, bit - 1, son[x][c ^ 1], son[y][c ^ 1]);
return query(v, bit - 1, son[x][c], son[y][c]);
}
int p[N], q[N], id[N], mx;
struct MonotoneStack {
int stc[N], *T = stc;
void clear() {while(*T) T--;}
int top() {return *T;}
void upd(int p) {while(*T && a[*T] < a[p]) T--;}
void push(int p) {upd(p), *++T = p;}
} stc;
void solve() {
stc.clear(), node = 0;
for(int i = 1; i <= n; i++) modify(R[i - 1], R[i], a[i], B);
for(int i = 1; i <= n; i++) stc.upd(i), p[i] = stc.top(), stc.push(i), id[i] = i;
sort(id + 1, id + n + 1, [&](int x, int y) {return p[x] != p[y] ? p[x] < p[y] : x < y;}), stc.clear();
for(int i = 1, cur = 1; i <= n; i++) {
while(cur < p[id[i]]) stc.push(cur++);
stc.upd(id[i]), q[id[i]] = stc.top();
}
for(int i = 1; i <= n; i++) {
if(a[i] == mx) continue;
cmax(ans, query(a[i], B, R[q[i]], R[i]));
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i], cmax(mx, a[i]);
solve(), reverse(a + 1, a + n + 1), solve(), cout << ans << endl;
return cerr << "Time : " << clock() << endl, flush(), 0;
}
*IV. P3045 [USACO12FEB]Cow Coupons G
好题。自己想的思路假了很多次,最后还是看了题解。
首先可以证明前
核心思想:考虑加入每个元素时不同策略的代价,通常可以使用用数据结构维护保证我们每次取出更小的代价。另一个经典应用:April Fool’s problem (medium & hard).
const int N = 5e4 + 5;
ll n, k, m, ans, cst, p[N], c[N], vis[N];
priority_queue <pii, vector <pii>, greater <pii>> q1, q2, q3;
int main(){
cin >> n >> k >> m, ans = k;
for(int i = 1; i <= n; i++) q2.push({p[i] = read(), i}), q3.push({c[i] = read(), i});
for(int i = 1, v; i <= k; i++) {
pii buy = q3.top(); q3.pop();
if(m >= buy.fi) vis[v = buy.se] = 1, m -= buy.fi, q1.push({p[v] - c[v], v});
else cout << i - 1 << endl, exit(0);
}
while(ans < n) {
while(vis[q2.top().se]) q2.pop();
while(vis[q3.top().se]) q3.pop();
if(q2.top().fi < q3.top().fi + q1.top().fi) {
pii buy = q2.top();
if(m < buy.fi) break;
ans++, q2.pop(), vis[buy.se] = 1, m -= buy.fi;
} else {
pii buy = q3.top(), rep = q1.top();
if(m < buy.fi + rep.fi) break;
ans++, q3.pop(), q1.pop(), vis[buy.se] = 1;
m -= buy.fi + rep.fi, q1.push({p[buy.se] - c[buy.se], buy.se});
}
}
cout << ans << endl;
return flush(), 0;
}
*V. [BZOJ2264]Free Goodies
仍然是神仙题。由于 Petra 的策略相对固定,即如果按照
不妨设 P 先手,考虑 P 和 J 按顺序选取每一个礼物,那么 J 第 set
或线段树维护。对于 J 先手同理,有
const int N = 1e3 + 5;
const int inf = 1e9 + 7;
struct stuff {
int a, b;
bool operator < (const stuff &v) const {
return a != v.a ? a > v.a : b < v.b;
}
} c[N];
struct data {
int p, val;
friend data operator + (data u, data v) {
if(u.val != v.val) return u.val < v.val ? v : u;
return c[u.p].a < c[v.p].a ? u : v;
}
} val[N << 2];
char s[N];
int n, x, y, vis[N];
void build(int l, int r, int x) {
if(l == r) return val[x] = {l, c[l].b}, void();
int m = l + r >> 1;
build(l, m, x << 1), build(m + 1, r, x << 1 | 1);
val[x] = val[x << 1] + val[x << 1 | 1];
}
void modify(int l, int r, int p, int x) {
if(l == r) return val[x] = {l, -inf}, void();
int m = l + r >> 1;
if(p <= m) modify(l, m, p, x << 1);
else modify(m + 1, r, p, x << 1 | 1);
val[x] = val[x << 1] + val[x << 1 | 1];
}
data query(int l, int r, int ql, int qr, int x) {
if(ql <= l && r <= qr) return val[x];
int m = l + r >> 1; data ans = {-1, -1};
if(ql <= m) ans = ans + query(l, m, ql, qr, x << 1);
if(m < qr) ans = ans + query(m + 1, r, ql, qr, x << 1 | 1);
return ans;
}
void solve() {
scanf("%d %s", &n, s + 1), mem(vis, 0, N), x = y = 0;
for(int i = 1; i <= n; i++) scanf("%d %d", &c[i].a, &c[i].b);
sort(c + 1, c + n + 1), build(1, n, 1);
int p = s[1] == 'P' ? 2 : 1;
while(p + 2 <= n) p += 2;
while(p > n) p -= 2;
while(p >= 1) {
data res = query(1, n, p, n, 1);
y += res.val, modify(1, n, res.p, 1), vis[res.p] = 1, p -= 2;
}
for(int i = 1; i <= n; i++) x += (!vis[i]) * c[i].a;
cout << x << " " << y << endl;
}
int main(){
int T; cin >> T;
while(T--) solve();
return flush(), 0;
}
启示:两个人在博弈时,若一个人的决策顺序相对固定,为了使另一个人最优可以将问题转化为括号序列匹配,利用
*VI. P4647 [IOI2007] sails 船帆
思维谔谔题(sweet)。不难发现桅杆的顺序对最终答案并没有影响,以及我们要尽量让帆的分布尽量均匀,即
但如果先考虑高度更大的桅杆,我们不知道如何平衡相较于别的桅杆多出的那一部分高度挂的帆的个数与剩下部分的挂的帆的个数。于是这引出了另一个核心思想 & 启示:在限制条件较多且满足包含关系时,优先考虑限制较严格的条件。
因此按高度从小到大排序并考虑每个桅杆
const int N = 1e5 + 5;
const int inf = 1e9;
ll laz[N], val[N], sum[N];
int R, rd[N], ls[N], rs[N], sz[N];
void push(int x) {
sum[x] = val[x] + sum[ls[x]] + sum[rs[x]];
sz[x] = sz[ls[x]] + sz[rs[x]] + 1;
}
void tag(int x, ll v) {sum[x] += sz[x] * v, val[x] += v, laz[x] += v;}
void down(int x) {
if(!laz[x]) return;
if(ls[x]) tag(ls[x], laz[x]);
if(rs[x]) tag(rs[x], laz[x]);
laz[x] = 0;
}
int merge(int x, int y) {
if(!x || !y) return x | y;
down(x), down(y);
if(rd[x] > rd[y]) return rs[x] = merge(rs[x], y), push(x), x;
return ls[y] = merge(x, ls[y]), push(y), y;
}
void splitv(int p, int &x, int &y, int v) {
if(!p) return x = y = 0, void();
down(p);
if(v >= val[p]) splitv(rs[p], rs[x = p], y, v);
else splitv(ls[p], x, ls[y = p], v);
push(p);
}
void splitk(int p, int &x, int &y, int k) {
if(!p) return x = y = 0, void();
down(p);
if(k <= sz[ls[p]]) splitk(ls[p], x, ls[y = p], k);
else splitk(rs[p], rs[x = p], y, k - sz[ls[p]] - 1);
push(p);
}
int Gmin(int p) {
if(!p) return inf;
while(1) {down(p); if(ls[p]) p = ls[p]; else return val[p];}
assert(0);
}
int Gmax(int p) {
if(!p) return -inf;
while(1) {down(p); if(rs[p]) p = rs[p]; else return val[p];}
assert(0);
}
ll n, ans;
struct sail {
int h, k;
bool operator < (const sail &v) const {
return h < v.h;
}
} s[N];
void modify(int k) {
int y, z, yy, zz; splitk(R, y, z, k), ans += sum[y];
int mx = Gmax(y), mn = Gmin(z); tag(y, 1);
if(mx != mn) return R = merge(y, z), void();
splitv(y, y, yy, mx), splitv(z, zz, z, mn);
R = merge(merge(y, zz), merge(yy, z));
}
int main(){
cin >> n, srand(time(0));
for(int i = 1; i <= n; i++) s[i].h = read(), s[i].k = read();
sort(s + 1, s + n + 1);
for(int i = 1; i <= n; i++) {
for(int p = s[i - 1].h + 1; p <= s[i].h; p++)
sz[p] = 1, rd[p] = rand(), R = merge(p, R);
modify(s[i].k);
} cout << ans << endl;
return flush(), 0;
}
VII. [BZOJ4209]西瓜王
题意有点难懂,大概就是从一段区间
若区间前
const int N = 3e5 + 5;
const int K = N * 20;
int n, T, k, a[N], d[N];
int node, R[N], ls[K], rs[K];
struct Data {
ll odd, even;
Data friend operator - (Data x, Data y) {return {x.odd - y.odd, x.even - y.even};}
Data friend operator + (Data x, Data y) {return {x.odd + y.odd, x.even + y.even};}
Data friend operator * (Data x, int y) {return {x.odd * y, x.even * y};}
void init(int _odd, int _even) {odd += _odd, even += _even;}
} val[K], sum[K];
void init(int x, int v) {
if(v & 1) val[x].init(1, 0), sum[x].init(v, 0);
else val[x].init(0, 1), sum[x].init(0, v);
}
void modify(int pre, int &x, int l, int r, int p) {
ls[x = ++node] = ls[pre], rs[x] = rs[pre];
if(l == r) return val[x] = val[pre], sum[x] = sum[pre], init(x, d[p]);
int m = l + r >> 1;
if(p <= m) modify(ls[pre], ls[x], l, m, p);
else modify(rs[pre], rs[x], m + 1, r, p);
val[x] = val[ls[x]] + val[rs[x]], sum[x] = sum[ls[x]] + sum[rs[x]];
}
pair <Data, Data> query(int x, int y, int k) {
int l = 1, r = n;
Data ans = {0, 0}, tot = {0, 0};
while(l < r) {
int m = l + r >> 1;
Data son = val[rs[y]] - val[rs[x]];
if(k <= son.even + son.odd) l = m + 1, x = rs[x], y = rs[y];
else
ans = ans + son, tot = tot + sum[rs[y]] - sum[rs[x]],
r = m, x = ls[x], y = ls[y], k -= son.even + son.odd;
}
return {ans + (d[l] & 1 ? (Data){k, 0} : (Data){0, k}),
tot + (d[l] & 1 ? (Data){k * d[l], 0} : (Data){0, k * d[l]})};
}
ll queryodd(int x, int y, int k) {
ll l = 1, r = n;
while(l < r) {
int m = l + r >> 1, sz = val[rs[y]].odd - val[rs[x]].odd;
if(k <= sz) l = m + 1, x = rs[x], y = rs[y];
else r = m, x = ls[x], y = ls[y], k -= sz;
}
return d[l];
}
ll queryeven(int x, int y, int k) {
ll l = 1, r = n;
while(l < r) {
int m = l + r >> 1, sz = val[rs[y]].even - val[rs[x]].even;
if(k <= sz) l = m + 1, x = rs[x], y = rs[y];
else r = m, x = ls[x], y = ls[y], k -= sz;
}
return d[l];
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++) d[i] = a[i] = read();
sort(d + 1, d + n + 1);
for(int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + n + 1, a[i]) - d;
for(int i = 1; i <= n; i++) modify(R[i - 1], R[i], 1, n, a[i]);
T = read();
while(T--) {
int l = read(), r = read(); k = read();
if(!k) {print(0), pc('\n'); continue;}
Data tmp = val[R[r]] - val[R[l - 1]];
if((tmp.odd >> 1) + (tmp.even >> 1) < k >> 1) {print(-1), pc('\n'); continue;}
pair <Data, Data> res = query(R[l - 1], R[r], k);
Data num = res.fi; ll val = res.se.odd + res.se.even;
if(num.even & 1) {
ll ans = 0;
if(num.even != tmp.even) {
ll veven = queryeven(R[l - 1], R[r], num.even + 1);
ll vodd = queryodd(R[l - 1], R[r], num.odd);
cmax(ans, val + veven - vodd);
}
if(num.odd != tmp.odd) {
ll veven = queryeven(R[l - 1], R[r], num.even);
ll vodd = queryodd(R[l - 1], R[r], num.odd + 1);
cmax(ans, val + vodd - veven);
}
print(ans), pc('\n');
}
else print(val), pc('\n');
}
return flush(), 0;
}
3. 神仙思路贪心题大赏
*I. P5912 [POI2004]JAS
首先对问题进行转化:相当于我们需要求原树最浅的一个点分树深度。假设点
考虑这样一个贪心:我们记
根据点分树的结论答案不可能超过
const int N = 5e4 + 5;
const int K = 1 << 16;
int cnt, hd[N], nxt[N << 1], to[N << 1];
void add(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v;}
int n, f[N], S[N], lg[K], buc[K];
void dfs(int id, int fa) {
int msk = 0, ban = 0, leaf = 1;
for(int i = hd[id]; i; i = nxt[i]) {
int it = to[i];
if(it == fa) continue;
leaf = 0, dfs(it, id), f[id] = max(f[id], f[it]);
msk |= ban & S[it], ban |= S[it];
} if(leaf) return S[id] = 1, void();
msk = (K - (1 << lg[msk])) & (K - 1 - ban);
int c = !msk ? lg[n] + 1 : buc[msk & -msk];
f[id] = max(f[id], c);
S[id] = (ban & (K - (1 << c))) | (1 << c);
}
int main(){
cin >> n;
for(int i = 2; i < K; i++) lg[i] = lg[i >> 1] + 1;
for(int i = 1; i < 16; i++) buc[1 << i] = i;
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add(u, v), add(v, u);
} dfs(1, 0), cout << f[1] << endl;
return 0;
}
启示:遇到无从下手的问题时先尝试抽象问题并分析性质(本题中是两点间的简单路径必然存在点分树的 LCA)使其更好理解。树上问题先从叶子开始,可以先考虑一些简单树再尝试总结策略。树形 DP 先考虑仅合并两个子树的情况。
*II. P3269 [JLOI2016]字符串覆盖
神仙思维题。一个时空复杂度均非常优秀的解法。
显然对于最大值和最小值需要分开计算。首先我们求出一些基础的东西辅助解题:
最大值:
遇到这种题目我们似乎无从下手,那么尝试把
当然可以更优:用 lower_bound
代替线性查找即可做到
最小值:
一个显然的想法是舍弃所有被其它字串覆盖的子串,若相同则仅保留一个,因为要使答案最小让其被完全覆盖一定最优。那么剩下来的子串就一定满足若
当
也许你会问:直接用求最小值的 DP 求最大值不就行了吗?非也,因为转移方程中
复杂度分析:本题的时间复杂度为
#include <bits/stdc++.h>
using namespace std;
#define mem(x, v, s) memset(x, v, sizeof(x[0]) * (s))
template <class T1, class T2> void cmin(T1 &a, T2 b){a = a < b ? a : b;}
template <class T1, class T2> void cmax(T1 &a, T2 b){a = a > b ? a : b;}
const int N = 1e4 + 5;
char t[N], s[4][N];
int n, tL, len[4], nxt[4][N];
bool mat[4][N];
void KMP(char *s, int sL, int *nxt, bool *mat) {
for(int i = 2; i <= sL; i++) {
nxt[i] = nxt[i - 1];
while(nxt[i] && s[nxt[i] + 1] != s[i]) nxt[i] = nxt[nxt[i]];
if(s[nxt[i] + 1] == s[i]) nxt[i]++;
}
for(int i = 1, p = 0; i <= tL; i++) {
while(p && s[p + 1] != t[i]) p = nxt[p];
if(s[p + 1] == t[i]) p++;
if(p == sL) mat[i] = 1, p = nxt[p];
else mat[i] = 0;
}
}
bool OverLap(char *t, char *s, int tL, int sL, int *nxt) {
for(int i = 1, p = 0; i <= tL; i++) {
while(p && s[p + 1] != t[i]) p = nxt[p];
if(s[p + 1] == t[i]) p++;
if(p == sL) return 1;
}
return 0;
}
int GetMax() {
if(n == 1) return len[0];
static int id[4], ans, pos[4][N], cnt[4]; ans = 0, mem(cnt, 0, 4);
for(int i = 0; i < n; i++) id[i] = i;
for(int i = 0; i < n; i++) for(int j = 1; j <= tL; j++) if(mat[i][j]) pos[i][cnt[i]++] = j - len[i];
do {
for(int S = 0; S < 1 << n - 1; S++) {
int cur = -1, res = 0, rbound = 0;
for(int bit = 0; bit < n; bit++) {
int i = id[bit];
if(!bit) {cur = pos[i][0], rbound = cur + len[i] - 1, res = len[i]; continue;}
int p = -1, pr = id[bit - 1];
if(S >> bit - 1 & 1) {
int rlim = min(tL - len[i] + 1, cur + len[pr] - 1);
int it = upper_bound(pos[i], pos[i] + cnt[i], rlim) - pos[i];
if(it == 0 || pos[i][it - 1] < cur) break;
p = pos[i][it - 1];
}
else {
int it = lower_bound(pos[i], pos[i] + cnt[i], cur + len[pr]) - pos[i];
if(it == cnt[i]) break;
p = pos[i][it];
}
res += max(0, p + len[i] - 1 - max(rbound, p - 1));
cmax(rbound, p + len[i] - 1), cur = p;
}
cmax(ans, res);
}
} while(next_permutation(id, id + n));
return ans;
}
int GetMin() {
if(n == 1) return len[0];
static int ban[4], id[4], m; mem(ban, 0, 4), m = 0;
for(int i = 0; i < n; i++) for(int j = 0; j < n; j++)
if(strcmp(s[i] + 1, s[j] + 1)) ban[j] |= OverLap(s[i], s[j], len[i], len[j], nxt[j]);
for(int i = 0; i < n; i++) for(int j = i + 1; j < n; j++) ban[j] |= !strcmp(s[i] + 1, s[j] + 1);
for(int i = 0; i < n; i++) if(!ban[i]) id[m++] = i;
static int f[2][16], g[2][16];
mem(f, 0x3f, 2), mem(g, 0x3f, 2), f[0][0] = g[0][0] = 0;
for(int i = 1, cur = 1, pr = 0; i <= tL; i++, swap(cur, pr)) {
for(int j = 0; j < 1 << m; j++) {
f[cur][j] = N;
for(int k = 0; k < j; k++) {
if(!(j >> k & 1)) continue;
int S = j - (1 << k), p = id[k];
if(mat[p][i]) cmin(f[cur][j], min(f[pr][S] + len[p], g[pr][S] + i));
}
g[cur][j] = min(g[pr][j], f[cur][j] - i), cmin(f[cur][j], f[pr][j]);
}
}
return f[tL & 1][(1 << m) - 1];
}
void solve() {
scanf("%s %d", t + 1, &n), tL = strlen(t + 1);
for(int i = 0; i < n; i++) {
scanf("%s", s[i] + 1);
KMP(s[i], len[i] = strlen(s[i] + 1), nxt[i], mat[i]);
}
cout << GetMin() << " " << GetMax() << "\n";
}
int main(){
int T; cin >> T;
while(T--) solve();
return 0;
}
启示:在时间复杂度可以承受的前提下尽可能确定更多信息,也许其所带来的重要性质使 DP 或贪心变得可行。
*III. P2587 [ZJOI2008]泡泡堂
还算有趣的题目,经过 ycx 的点拨后豁然开朗。
求最小值就是用
考虑我们有
现在只需要求每个
const int N = 1e5 + 5;
int n, a[N], b[N];
int solve(int *a, int *b) {
static int res[N << 1], ans; mem(res, 0, N), ans = 0;
for(int i = n, p = n, q = n; i; i--) {
while(p && b[p] > a[i]) p--;
while(q && b[q] >= a[i]) q--;
int c = n - i + 1;
res[c] += 2, res[c + p]--, res[c + q]--;
}
for(int i = 1; i <= n; i++) cmax(ans, res[i] += res[i - 1]);
return ans;
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= n; i++) b[i] = read();
sort(a + 1, a + n + 1), sort(b + 1, b + n + 1);
cout << solve(a, b) << " " << 2 * n - solve(b, a) << endl;
return 0;
}
看完题解后学会了一个更为简洁的做法:若当前
const int N = 1e5 + 5;
int n, a[N], b[N];
int solve(int *a, int *b) {
int ans = 0, al = 1, bl = 1, ar = n, br = n;
for(int i = 1; i <= n; i++) {
if(a[al] > b[bl]) ans += 2, al++, bl++;
else if(a[ar] > b[br]) ans += 2, ar--, br--;
else ans += a[al] == b[br], al++, br--;
}
return ans;
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= n; i++) b[i] = read();
sort(a + 1, a + n + 1), sort(b + 1, b + n + 1);
cout << solve(a, b) << " " << 2 * n - solve(b, a) << endl;
return 0;
}
启示:很多题目都是相交劣于不交,可以利用这个性质解题。多尝试运用调整法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现