贪心

1. 模拟费用流贪心(可撤销贪心)

一个非常玄妙的算法。

*I. CF280D k-Maximum Subsequence Sum

一道用数据结构维护的模拟费用流贪心。首先当 \(k=1\) 时我们显然选择区间最大子段和。但 \(k>1\) 时首先选择区间最大子段和并不一定最优。

这个问题一脸可以费用流的样子:建立 \(n+1\) 个点 \(1,2,\cdots,n+1\),第 \(i\) 个点向第 \(i+1\) 个点连一条流量为 \(1\),费用为 \(a_i\) 的边。询问 \(l,r,k\) 相当于点 \(l\sim r+1\) 向汇点 \(T\) 连一条流量为 \(1\),费用为 \(0\) 的边,超级源点 \(S\) 向源点 \(S'\) 连一条流量为 \(k\),费用为 \(0\) 的边,源点 \(S'\) 向点 \(l\sim r+1\) 连一条流量为 \(1\),费用为 \(0\) 的边,求最大费用最大流。

显然直接流是不可接受的,考虑模拟费用流:注意到每次从 \(S\)\(T\) 找最长路相当于求 \(l\sim r\) 的最大子段和(可以为空),不妨设为 \([l',r']\)。然后需要将权值取反表示反边流量 \(+1\),正边流量 \(-1\)。做 \(k\) 次上述操作即为答案。因此,线段树维护区间取反(需要实时维护每个区间取反后的信息,这个 trick 在平衡树专题 FHQ treap 部分例题 [NOI2021] 密码箱有提到过),区间最大子段和及其端点即可。时间复杂度 \(\mathcal{O}(m\log n+qk\log n)\)\(m\) 是修改次数,\(q\) 是询问次数。

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] 数据备份

一个初步想法是每次选代价最小的连,但这样不一定最优,因为与它相邻的两段就不能被选,可能导致我们因为 \(k\) 的限制必须选择一个代价更大的连。但这种情况仅出现在需要舍掉当前 \(i\) 并选择其两端 \(i-1,i+1\) 连边时:因为对于不包含 \(i+1\) 但包含 \(i-1\) 的连边情况,若 \(i-1\)\(i\) 代价更小则贪心策略会优先选到 \(i-1\) 而非 \(i\),若 \(i\)\(i-1\) 代价更小则将 \(i-1\) 换成 \(i\) 不影响选择的合法性且会让答案不劣。对于包含 \(i+1\) 但不包含 \(i-1\) 的连边亦然。也就是说如果这个贪心策略单独选择了 \(i\),那么 \(i-1\)\(i+1\) 在最优方案中必不可能只出现一个。对于连续间隔选择的一段区间同理。

上述结论让这个贪心策略的反悔变得容易:如果选择了 \(i\),那么将 \(d_{i-1}+d_{i+1}-d_i\) 加入小根堆,选择则表示将 \(i\) 撤销掉并选择 \(i-1\)\(i+1\)更一般的,如果选择了连续间隔的一段 \(l,l+2,\cdots,r\) 且其贡献为 \(c\),那么它的反悔方案代价 \(c_i=c_{pre_i}+c_{suf_i}-c\ \left(c=\dfrac{l+r}2\right)\)。若选择这个决策就将 \(c_i\) 加入答案(即通过小根堆选最小的 \(c_i\))并删除 \(pre_i\)\(suf_i\)\(pre\)\(suf\) 用双向链表维护。时间复杂度 \(\mathcal{O}(n\log n)\)

模拟费用流:注意到每条边必然连接相邻的两个点,因此将所有点按照编号奇偶性分类:从超级源点 \(S\) 向源点 \(S'\) 连一条流量为 \(k\),费用为 \(0\) 的边限制流量,从源点 \(S'\)\(1,3,5,\cdots,2d+1\ (2d+1\leq n)\) 连一条流量为 \(1\),费用为 \(0\) 的边,从 \(2i+1\)\(2i\)\(2i+2\) 连一条流量为 \(1\), 费用分别为 \(s_{2i+1}-s_{2i}\)\(s_{2i+2}-s_{2i+1}\)(若不存在则不连),从 \(2,4,6,\cdots,2d\) 向汇点连一条流量为 \(1\),费用为 \(0\) 的边,则这张图的 MCMF 就是答案。实际上在这张图上的找最短路增广取反就是上述做法中取出小根堆堆顶,取相反数后向两边扩展的过程。

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

首先忽略所有顾客,计算出每天结束时拥有的货物数量即 \(s_i\) 的前缀和。为了使满足需求的顾客数量最多,我们按照 \(b_i\) 从小到大排序。若当前顾客可以被满足,即 \(b_i\geq \min_{j=p_i}^n s_j\) 那么就满足,并将 \(s_{p_i}\sim s_n\) 减去 \(b_i\) 表示从顾客到来的时间 \(p_i\) 到最后的每一天晚上货物的拥有数量都要少掉 \(b_i\)。可以用线段树维护区间加减与区间最值。

另一种解法:从左往右考虑每个顾客 \(i\),若当前剩余货物能满足就满足,否则若已经满足的顾客的 \(b\) 的最大值 \(>b_i\),显然将最大值更新为 \(b_i\) 更优。用大根堆维护。

两种解法的时间复杂度均为 \(\mathcal{O}(n\log n)\),前者常数大一些。本题的核心思想在于:贪心选择 \(b_i\) 更小的顾客。

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]乌鸦喝水

神仙题。首先对问题进行初步分析:乌鸦肯定是能喝就喝,因为留到后面再喝不会让局面变得更优(贪心思想所在)。因此本题变成了一道萌萌模拟题,但 \(\mathcal{O}(nm)\) 的复杂度不可接受,思考如何优化。

接下来的部分看了题解:设 \(d_i=\dfrac{x-w_i}{a_i}+1\) 表示每个水罐最多能贡献第 \(d_i\) 次喝水,其中除法下取整。显然的结论是:若 \(d_i\) 更小的水罐能被喝到,则 \(d_i\) 更大的水罐也一定能(本题的核心思想)。因此,考虑将所有水罐按 \(d_i\) 从小到大排序,然后依次考虑每个水罐 \(i\)

设当前答案为 \(a\),现在是第 \(r\) 轮喝水且乌鸦当前在第 \(las\) 个水罐处。求出从 \(las\)\(n\) 还有多少水罐存活,记为 \(cnt\)。若 \(a+cnt< d_i\) 则乌鸦可以喝完这 \(cnt\) 个水罐,令 \(a\gets a+cnt\)\(r\gets r+1\)\(las\gets 1\) 表示新开一轮。否则二分找到从 \(las\) 开始第 \(d_i-a\) 个存活的水罐的位置,设为 \(p\),那么乌鸦在从 \(las\) 喝到 \(p\) 时,\(d_i\) 就无用了,因为此时的答案 \(a\) 已经等于 \(d_i\),这意味着 \(d_i\) 不会再对答案产生任何贡献。我们将 \(d_i\) 所表示的水罐 \(j\) 标记为非存活水罐,然后继续下一个水罐 \(d_{i+1}\) 直到所有水罐都被喝完或 \(r>m\)

说简单点,就是我们通过关注当前存活的水罐中 \(d\) 值最小的那一个并实时动态维护每个水罐的存活情况从而加速模拟。用 BIT 可以支持上述所有操作:单点修改,区间求和以及二分一个权值不大于某个阈值的最大位置(BIT 上倍增)。时间复杂度 \(\mathcal{O}((n+m)\log n)\)

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

考虑枚举作为区间第二大值的元素 \(a_i\),忽略最大值。单调栈找到在 \(i\) 左边第二个大于 \(a_i\) 的数的位置 \(p\)(如果不存在就是 \(0\)),那么在 \(p+1\sim i\) 之间的任何数都能被选到,因为如果包含 \([p,i]\) 那么 \(i\) 就不是第二大了,而 \([p+1,i]\) 就是一个合法区间。特别的,若 \(p=0\) 则可以将右端点向右扩展直到 \(a_i\) 成为第二大,因为 \(a_i\) 不是最大值所以合法区间必然存在。故 \(a_i\) 可以异或上任何 \(a_j\ (j\in[p+1,i])\),求一段区间异或某个数的最大值用可持久化 trie。右边同理。时空复杂度 \(\mathcal{O}(n\log V)\)

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

好题。自己想的思路假了很多次,最后还是看了题解。

首先可以证明前 \(k\) 头牛一定被买(反证法),但不一定在它们身上使用优惠券。接下来考虑加入一头牛 \((c_i,p_i)\)。若 \(c_j+p_i>c_i+p_j\)\(p_i>c_i+p_j-c_j\) 则需重新分配优惠券并花 \(p_j-c_j+c_i\)。否则要花费 \(p_i\) 的代价买下这头牛。因此维护未买奶牛\(p_i\)\(c_i\)已买奶牛\(p_j-c_j\) 的小根堆,每次看究竟是最小的 \(p_j-c_j+c_i\) 更小还是最小的 \(p_i\) 更小。注意实时更新每头奶牛的状态,时间复杂度 \(\mathcal{O}(n\log n)\)。比较类似模拟费用流贪心。

核心思想:考虑加入每个元素时不同策略的代价,通常可以使用用数据结构维护保证我们每次取出更小的代价。另一个经典应用: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_i\) 为第一关键字从大到小,\(j_i\) 为第一关键字从小到大排序,那么 P 一定会选取第一个未被选择的礼物。而 Jan 就不一样了,因为当前的决策可能只是局部最优解而非全局最优。

不妨设 P 先手,考虑 P 和 J 按顺序选取每一个礼物,那么 J 第 \(i\) 次操作选择的是第 \(2i\) 个礼物。从小到大考虑每一次操作选取的礼物编号 \(p_i\),初始值为 \(2i\):J 有能力把 \(p_i\) 向右移,但无法左移,因为 P 的策略是取最左边的礼物。换句话来说,\(p_i\) 从小到大排序后一定有 \(p_i\geq 2i\)。反证法可以证明其必要性,构造法可以证明其充分性。因此从右往左(即按 \(i\) 从大到小)每次贪心选择最大的礼物即可,可以用 set 或线段树维护。对于 J 先手同理,有 \(p_i\geq 2i-1\)。时间复杂度 \(\mathcal{O}(Tn\log n)\)

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

启示:两个人在博弈时,若一个人的决策顺序相对固定,为了使另一个人最优可以将问题转化为括号序列匹配,利用 \(p_i\geq 2i\) 的性质解题

*VI. P4647 [IOI2007] sails 船帆

思维谔谔题(sweet)。不难发现桅杆的顺序对最终答案并没有影响,以及我们要尽量让帆的分布尽量均匀,即 \(2,4\) 劣于 \(3,3\),根据 \(\dbinom{i}{2}\) 的凸性可证。

但如果先考虑高度更大的桅杆,我们不知道如何平衡相较于别的桅杆多出的那一部分高度挂的帆的个数与剩下部分的挂的帆的个数。于是这引出了另一个核心思想 & 启示:在限制条件较多且满足包含关系时,优先考虑限制较严格的条件

因此按高度从小到大排序并考虑每个桅杆 \(i\),找到前 \(H_i\) 个高度中所放置的船帆个数前 \(K_i\) 小的,求和类计入答案并将这些高度的帆个数 \(+1\)。可以用平衡树(FHQ treap)维护,前缀 \(+1\) 时若第 \(K_i\) 小和第 \(K_{i+1}\) 小的值相同,设为 \(v\),则需要将 \(K_i\) 前缀按值 \(v-1\) 分裂成 \(T_{lp}\ (\forall x\in T_{lp},val_x<v)\)\(T_{rp}\ (\forall x\in T_{rp},val_x=v)\)\(H_i-K_i\) 后缀按值 \(v\) 分裂成 \(T_{ls}\)\(T_{rs}\),为 \(T_{lp}\)\(T_{rp}\) 打上 tag 后为了保持有序性,四棵树之间的顺序从小到大应变为 \(lp,ls,rp\)\(rs\)。时间复杂度 \(\mathcal{O}((n+H)\log H)\)

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]西瓜王

题意有点难懂,大概就是从一段区间 \([l,r]\) 内选出 \(k\ (2\mid k)\) 个数,满足奇数和偶数的个数都是偶数且总和最大。

若区间前 \(k\) 大有偶数个奇数和偶数,那么它们的和就是答案。否则答案为去掉前 \(k\) 大最小的奇数并加上非前 \(k\) 大的最大偶数,或者去掉点 \(k\) 大最小的偶数并加上非前 \(k\) 大的最大奇数后的最大值。注意若非前 \(k\) 大的奇数或偶数不存在则该情况不能计入贡献。时间复杂度 \(\mathcal{O}((n+q)\log n)\)

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

首先对问题进行转化:相当于我们需要求原树最浅的一个点分树深度。假设点 \(i\) 在倒数第 \(d_i+1\) 次被问到,那么任意两个 \(d\) 值相同的点 \(u,v\) 之间的简单路径必然存在一个点 \(a\) 使得 \(d_a>d_u\),因为这样才能使它们进入不同的点分树子树,说人话即 \(a\) 是深度相同的点 \(u,v\) 在点分树上的 LCA(的祖先)。

考虑这样一个贪心:我们记 \(S_i\) 表示 \(i\) 的子树内所有可能不满足条件的 \(d\) 值,即存在 \(u\in \mathrm{subtree}(i)\) 使得不存在 \(v\in \mathrm{path}(u,i)\) 满足 \(d_v>d_u\) 的所有 \(d_u\) 的集合,以二进制状压形式存储。合并 \(i\) 的两个子树 \(u,v\) 时,首先 \(d_i\) 应大于任何一个既在 \(S_u\) 又在 \(S_v\) 内的元素,否则显然不合法。此外,\(d_i\) 还不应存在于 \(S_u\)\(S_v\) 中,我们选择所有满足条件的最小的 \(d_i\) 即可。\(S_i\) 即所有 \(S_u\)\(\{d_i\}\) 的并去掉所有 \(<d_i\) 的元素后的集合。

\[d_i=\min \{c\mid c>\max\{x\mid x\in S_u\cap S_v\}\land c\notin S_u\}\\ S_i=\{d_i\}\cup\left\{x\left|\ x\geq d_i\land x\in\bigcup_{u\in\mathrm{son}(i)}S_u\right.\right\} \]

根据点分树的结论答案不可能超过 \(\log n\),因此时间复杂度 \(\mathcal{O}(n\log n)\),可以做到线性:通过位运算我们求出可行的 \(d_i\) 集合,取 \(\mathrm{lowbit}\) 即可,拿到了最优解。

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]字符串覆盖

神仙思维题。一个时空复杂度均非常优秀的解法

显然对于最大值和最小值需要分开计算。首先我们求出一些基础的东西辅助解题:\(n\) 个子串 \(s_i\) 的 next 数组(KMP)以及与母串 \(T\) 在每个位置的匹配情况。这个可以在 \(\mathcal{O}(nL)\) 的时间内预处理出来。

最大值:

遇到这种题目我们似乎无从下手,那么尝试把 \(n=4\) 作为突破口。考虑 \(n!\) 枚举钦定每个字符串出现位置按开头从左到右的顺序,那么一个贪心的想法是把出现顺序在前面的字符串尽量往前放。但这样有个问题,就是在放第 \(i\) 个字符串时有两种情况:是否与 \(s_{i-1}\) 重叠,因为两种情况都有可能成为最优解(反例容易举出)。但若确定了是哪种情况,贪心策略就保证了方案唯一:若不重叠,则越往前放越好(给剩下来的字符串留足空间);若重叠则越往后放越好(因为不劣)。因此再 \(2^{n-1}\) 枚举相邻的两个字符串是否重叠即可。注意统计答案是不应只关注前一个字符串,因为可能出现 \(l_1<l_2<r_2<l_3<r_3<r_1\) 的情况,其中 \(l_i,r_i\)\(s_i\)\(T\) 中的出现位置,因此需记录的是当前所有字符串的右端点最大值即 \(\max r_i\)。时间复杂度 \(\mathcal{O}(n!2^nnL)\)

当然可以更优:用 \(\log\) 级别的查找即 lower_bound 代替线性查找即可做到 \(\mathcal{O}(nL+n!2^nn\log L)\)

最小值:

一个显然的想法是舍弃所有被其它字串覆盖的子串,若相同则仅保留一个,因为要使答案最小让其被完全覆盖一定最优。那么剩下来的子串就一定满足若 \(l_i<l_j\) 则一定有 \(r_i<r_j\),这是很强的一个性质,并且结合最优化的限制,给予我们动态规划的思想:设 \(f_{i,S}\) 表示前 \(i\) 位放置了集合 \(S\) 内的子串的最小值且第 \(i\) 位被覆盖,转移时枚举 \(p\in S\)\(s_p\)\(T\)\(i\) 处匹配。分两种情况讨论,一种是与已放置字符串有交集,另一种是不交,综合一下转移方程如下:

\[\mathrm{checkmin}(f_{i,S},\min_{p\in S}\min_{0\leq j<i}f_{j,S\backslash p}+\min(len_p,i-j)) \]

\(j>i-len_p\)\(i-j<len_p\) 故进行贡献为 \(len_p\) 的转移不会使答案变得更小(即更优),而当 \(j\leq i-len_p\)\(i-j>len_p\) 所以进行贡献为 \(i-j\) 的转移也不会影响答案,因此可以看做对于每个 \(j\in [0,i)\) 都进行 \(len_p\)\(i-j\) 的转移。\(len_p\) 可以通过直接记录 \(f_{i,S}\) 前缀最小值优化,而 \(i-j\) 的转移可以设 \(g_{i,S}\) 表示 \(\min_{j=0}^if_{j,S}-j\) 进行优化。再加上滚动数组,本部分时间复杂度 \(\mathcal{O}(n2^nL)\),空间复杂度更是仅有惊人的 \(\mathcal{O}(2^n)\)

也许你会问:直接用求最小值的 DP 求最大值不就行了吗?非也,因为转移方程中 \(\min(len_p,i-j)\) 的部分并没有变成 \(\max\),故此时 \(len_p\) 只能从 \(j\leq i-len_p\) 转移,而 \(i-j\) 只能从 \(j>i-len_p\) 转移,所以需要加一个线段树维护区间修改与区间最值,很麻烦,不如直接贪心更方便。而且一道题目锻炼两种思维,岂不妙哉?

复杂度分析:本题的时间复杂度为 \(\mathcal{O}(n!n2^n\log L+n2^nL)\),空间复杂度为 \(\mathcal{O}(nL+2^n)\)。很显然后者已经达到了理论下界。实现起来不算麻烦,而且效率非常优秀,以 33ms 的极限速度与仅仅 900K 的空间占用夺得最优解。

#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 的点拨后豁然开朗。

求最小值就是用 \(2n\) 减掉对方得分最大值,因为双方得分和为定值 \(2n\)。故只需求出一方打另一方的最大值,不妨设己方战力从大到小排序后为 \(a_i\),对方为 \(b_i\)

考虑我们有 \(k\) 局非负,那么就是 \(a_i\) 的前 \(k\) 大打 \(b_i\) 的前 \(k\) 小,并且一定是 \(a_i\to b_{n-k+i}\),即 \(a\)\(i\) 大的打 \(b\)\(k\) 小中第 \(i\) 大的。由于匹配不可能交叉故有上述结论,可以用调整法证明。

现在只需要求每个 \(a_i\) 能平或胜多少个对手,分别记为 \(d,w\),显然当 \(k\in[i,i+w-1]\)\(a_i\) 对答案的贡献都是 \(2\)(因为若 \(a_i\)\(b_j\) 匹配则可以确定 \(k=i+n-j\),而当 \(k\in[i,i+w-1]\)\(a_i\) 一定与 \(b\) 中前 \(w\) 小之一匹配,故贡献为 \(2\)),当 \(k\in [i+w,i+d-1]\)\(a_i\) 对答案的贡献是 \(1\),差分维护即可,时间复杂度是排序的线性对数。

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

看完题解后学会了一个更为简洁的做法:若当前 \(\max a_i>\max b_j\) 则用 \(a\) 最大打 \(b\) 最大并弹出;若 \(\min a_i>\min b_j\) 同理。否则可以类似田忌赛马的思想用最弱打最强。为什么这样是对的呢?因为显然最大和最小值最好也只能平对方,但如果用最小值换对方最大值,己方最大值打对方最小值就可以获胜(如果是平则用最小值换最大值时也有 \(1\) 的贡献,不劣),而两个不大于 \(1\) 的数相加显然不大于 \(2\),故贪心策略正确。时间复杂度是排序的线性对数。

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

启示:很多题目都是相交劣于不交,可以利用这个性质解题。多尝试运用调整法

posted @ 2021-11-03 16:27  qAlex_Weiq  阅读(1389)  评论(1编辑  收藏  举报