邻项交换法
问题背景:给定 n 个元素 a1,a2,…,an,要求求出一个最优的排列顺序 p,使得 f(ap1,ap2,…,apn) 最优。
定义一种二元关系 ⪯:x⪯y ⟺ 对于任意的元素前缀和后缀,f(…,x,y,…) 都不比 f(…y,x…) 劣。
若这种关系是一种全预序(满足自反性,传递性,连接性):
- 自反性:x⪯x,根据定义显然永远成立。
- 传递性:∀a,b,c,若 a⪯b 且 b⪯c,则 a⪯c,需要根据序关系具体验证。
- 连接性:∀a,b,要么 a⪯b,要么 b⪯a,需要任意元素都可比。
定义 x∼y 表示 x⪯y 且 y⪯x(可以发现 ∼ 构成等价关系),记号 x≺y 表示 x⪯y 且 y⪯x 不成立(可以发现 ≺ 构成严格弱序)。
结论:则 p 是最优解 ⟺ ap1⪯ap2⪯…⪯apn。若多个元素之间大小相等,即 ai1∼ai2∼…∼aik,它们之间随便排列不会影响答案的最优性。
证明:
- (⇐)设 p∗ 为按 ⪯ 排序的有序排列。对于任意其他的排列 p,既然 ⪯ 满足全预序性质,我们总可以通过有限次交换相邻的逆序元素(即 x 在 y 前且 y≺x 的情况),将 p 调整为 p∗。根据 ⪯ 的定义,每一次这样的交换都会使 f 变优或保持不变。因此 f(p∗)≥f(p)。由于 p 是任意的,故 p∗ 是全局最优解。
- (⇒)用反证法,若存在 pi,pi+1 使得 api⪯api+1,根据连接性可知 api+1⪯api,而交换之后的解一定会更优(因为 api⪯api+1 不成立),所以 p 不是最优解,矛盾。
提示:虽然我们定义的是全预序,但调用 std::sort 的时候需要用它导出的一个严格弱序 ≺。
例题
题意
设有 n 个正整数 a1…an,求一个排列 p,使得 concat(ap1,ap2,…,apn) 最大。其中 concat 表示将若干个正整数用十进制表示写出(无前导零),按顺序拼接在一起后,以十进制解读得到的整数。
n≤20,ai≤109。
做法
这里对于相邻的元素只需要考虑局部的性质即可,因为前缀后缀怎么取并不会影响大小比较。
定义 ⪰:x≻y⟺concat(x,y)≥concat(y,x) ,然后排序即可。
具体来说 concat(x,y)≥concat(y,x) 恰好是把 x,y 看成字符串后 x+y≥y+x(其中 + 表示字符串拼接,≥ 表示按字典序比较),因为这两个数的长度相同。
这种关系显然具有连接性,我们验证它确实满足传递性。若 a+b≥b+a,可知 a×(10len(b)−1)≥b×(10len(a)−1),则 10len(a)−1a≥10len(b)−1b,那么定义 S(x)=10len(x)−1x,无非就是比较 S(x) 的大小,而实数的比较是具有传递性的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <bits/stdc++.h> using namespace std;
bool cmp(string a, string b) { return a + b > b + a; }
int main() { vector<string> v; string s; int n; cin >> n; int i; for (i = 0; i < n; i++) { cin >> s; v.push_back(s); } sort(v.begin(), v.end(), cmp); for (i = 0; i < n; i++) { cout << v[i]; } return 0; }
|
题意
给定 a,b 和 n 个二元组 (xi,yi),对于排列 p,定义
f(p)=i=1maxn⌊yia∏j=1i−1xi⌋
求 f(p) 最大值。
n≤1000,0<a,b,xi,yi<10000。
做法
仍然考虑邻项交换,假设现在的数是 (x1,y1) 和 (x2,y2),之前所有数的乘积是 p。
那么第一种方案(1,2)的贡献是:
max{⌊y1p⌋,⌊y2px1⌋}
第二种方案(2,1)的贡献是:
max{⌊y2p⌋,⌊y2px2⌋}
由于 x1,x2≥1,所以两个方案都会产生至少 ⌊y1/2p⌋ 的贡献,只需要比较 ⌊y2px1⌋ 和 ⌊y2px2⌋ 的大小。
知道向下取整是保序的,所以先不管取整。我们可以得到,若 x1y1≤x2y2,则 y2px1≤y2px2,也有 ⌊y2px1⌋≤⌊y2px2⌋,所以得证。
这个序关系的传递性是显然的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| n = int(input()) a,b = list(map(int, input().split())) p = [] for _ in range(n): x,y = list(map(int, input().split())) p.append((x,y))
p.sort(key=lambda t:t[0]*t[1])
ans = 0 mul = a for x,y in p: ans = max(ans, mul//y) mul *= x
print(ans)
|
题意
给定一颗 n 个点的有根树,每个点有权值 0/1,求出其的一个拓扑序(要求父亲在儿子之前出现),使得逆序数最少,求出这个逆序数。
n≤2×105。
做法
先考虑这样一个问题,有若干个 01 串,现在可以将它们随便排序,怎么排序能让最后的串逆序对数量最少。
考虑邻项交换,假设两个串里面的 01 数量分别是 (c0x,c1x), (c0y,c1y),第一种方案的代价是 c1x⋅c0y,第二种方案的代价是 c1y⋅c0x,因此 x 在 y 前面的条件就是 c1xc0x≥c1yc0y,按这个排序就能得到最优解。
然后对于这个问题来说,我们同样维护每个非根节点的 0/1 个数。对于 c1xc0x 最大的那个节点,一定是在选完根之后就立刻被选择的,所以我们可以直接把它们两个合并(儿子合并,0/1 个数相加),视作一个节点。然后不断进行这个过程,用一个并查集和优先队列维护即可。
复杂度 O(nlogn)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include<bits/stdc++.h> using namespace std;
const int N = 2e5+5; vector<int> G[N];
struct Node{ int c0, c1; int id; };
struct Comp{ bool operator()(const Node& lhs,const Node& rhs){ return 1ll * lhs.c0 * rhs.c1 < 1ll * rhs.c0 * lhs.c1; } };
priority_queue<Node, vector<Node>, Comp> pq;
int c0[N], c1[N]; int fa[N]; int find(int x){ return fa[x]==x?x:fa[x]=find(fa[x]); }
int treef[N];
int main(){ int n; cin>>n; fa[1] = 1; for(int i=2;i<=n;i++){ int f; cin>>f; G[i].push_back(f); G[f].push_back(i); treef[i] = f; fa[i] = i; }
for(int i=1;i<=n;i++){ int x; cin>>x; if(x==0)c0[i]=1; else c1[i]=1; if(i!=1)pq.push(Node{c0[i], c1[i], i}); } long long ans = 0;
while(pq.size()){ auto [x0,x1,u] = pq.top(); pq.pop(); u = find(u); if(x0 != c0[u] || x1 != c1[u])continue; int f = find(treef[u]); fa[u] = f; ans += 1ll * c1[f] * c0[u]; c1[f] += c1[u]; c0[f] += c0[u]; c1[u] = c0[u] = 0;
if(f!= 1)pq.push({c0[f], c1[f], f}); }
cout << ans << '\n';
return 0; }
|
练习
- ABC366F - Maximum Composition
- https://atcoder.jp/contests/abc434/tasks/abc434_f
- https://atcoder.jp/contests/abc225/tasks/abc225_f