题意
传送门 AtCoder ABC 328G Cut and Reorder
题解
假设答案对应的 a a a 下标 0 , 1 , ⋯ , n − 1 0,1,\cdots,n - 1 0,1,⋯,n−1 经过操作 1 变换为排列 p 0 , p 1 , ⋯ , p n − 1 p_{0},p_{1},\cdots,p_{n-1} p0,p1,⋯,pn−1,则对于满足 p i − 1 ≠ p i , 0 < i p_{i-1}\neq p_{i},0<i pi−1=pi,0<i 的位置,都至少要分割一次,且仅需这样的分割就可以得到 a p 0 , a p 1 , ⋯ , a p n − 1 a_{p_{0}},a_{p_{1}},\cdots,a_{p_{n-1}} ap0,ap1,⋯,apn−1。则状态压缩优化 p p p 的枚举即可。
不考虑分割贡献,则需要维护两个变量:集合 A = { a p 0 , a p 1 , ⋯ , a p m } \mathcal{A}=\{a_{p_{0}}, a_{p_{1}}, \cdots,a_{p_{m}}\} A={ap0,ap1,⋯,apm},以及集合匹配的 b b b 的前缀的长度 m m m。实际上,只需要维护前者,因为 ∣ A ∣ = m \lvert\mathcal{A}\rvert = m ∣A∣=m。
容易想到两类计算分割贡献的方案。一类是新增状态 a p i − 1 a_{p_{i-1}} api−1,转移时可以直接判断 a p i − 1 ≠ a p i a_{p_{i-1}}\neq a_{p_{i}} api−1=api,时间复杂度 O ( n 2 log n ) O(n^2\log n) O(n2logn)。另一种方案是枚举分割的段,此时每一个连续的段(除了起始段)操作 1 贡献是 c c c; A \mathcal{A} A 状态数为 2 n 2^n 2n,对于每一个状态,可能的段规模为 O ( n 2 ) O(n^2) O(n2),时间复杂度也是 O ( n 2 log n ) O(n^2\log n) O(n2logn);实际上,这是一个宽松的界,对于每一个段的长度 k k k,实际上对应的状态数为 2 n − k 2^{n-k} 2n−k,则一个更紧的界为 O ( ∑ k 2 n − k ) = O ( n log n ) O(\sum k2^{n-k})=O(n\log n) O(∑k2n−k)=O(nlogn)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
long long c;
cin >> c;
vector<long long> a(n), b(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
for (int i = 0; i < n; ++i) {
cin >> b[i];
}
auto get_min = [&](long long &x, long long y) {
x = min(x, y);
};
const long long inf = 1e18;
vector<long long> dp(1 << n, inf);
dp[0] = 0;
for (int i = 0; i < 1 << n; ++i) {
int bi = __builtin_popcount(i);
for (int l = 0, r = 0; l < n; l = r) {
if (i >> l & 1) {
r = l + 1;
continue;
}
while (r < n && (~i >> r & 1)) {
r += 1;
}
for (int ai = l; ai < r; ++ai) {
int st = 0;
long long add = 0;
for (int d = 1; ai + d <= r; ++d) {
st |= 1 << (ai + d - 1);
add += abs(a[ai + d - 1] - b[bi + d - 1]);
long long split = bi == 0 ? 0 : c;
get_min(dp[i | st], dp[i] + add + split);
}
}
}
}
cout << dp[(1 << n) - 1] << '\n';
return 0;
}