双指针
也叫尺取法,当两个变量的关系是当 \(i\) 变量增大时 \(j\) 变量只可能往一个方向走,那么我们就可以使用尺取法将 \(i,j\) 变量都只走一次,将 \(O(n^2)\) 优化到 \(O(n)\)。(这是在插入删除操作都 \(O(1)\) 的情况下。如果 \(O(n)\) 另当别论(埋坑:不带删的尺取)
一般二分法也可以解决这种单调的问题,但是时间复杂度不一样。
P1638
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
int n, m;
int a[1000010];
int num[2010], now;
int ansa, ansb = inf;
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
cin >> n >> m; f(i, 1, n) cin >> a[i];
num[a[1]]++; now = 1;
for(int i = 1, j = 1; i <= n; i++) {
while(now < m) {
if(j < n) {
j++;
num[a[j]]++; if(num[a[j]] == 1) now++;
}
else break;
}
if(now == m && j - i + 1 < ansb - ansa + 1) {ansa = i; ansb = j;}
num[a[i]]--; if(num[a[i]] == 0) now--;
}
cout << ansa << " " << ansb << endl;
return 0;
}
在 DP 转移中,经常使用双指针法优化一个维度时间复杂度。
CF1699E
题目大意:给定 \(n\) 个 \(1 \sim m\) 的数,可以将任意一个数分解成若干个数使得它们的乘积是这个数。求最后分解的数中最大数和最小数之差的最小值。
\(\sum n \le 10^6,\sum m \le 5 \times 10^6\)
分析:考虑 DP,设 \(dp[i][j]\) 表示 \(j\) 分解成一些数字使得这些数字不小于 \(i\),这些数字的最大值最小是多少。考虑转移: \(dp[i][j] = min(dp[i + 1][j], if(i | j \&\& i < j / i)dp[i][j / i])\)
外层枚举 \(i\),内层枚举 \(j\),时间复杂度 \(O(n^2)\)。
发现可以滚掉 \(i\) 这个维度,只转移 \(j/i\),那么我们就可以对于每个 \(i\) 只枚举 \(j \in [i \times i, \max{a_k}] \&\& i | j\),这个复杂度是 \(O(i \times \ln m)\) 的(调和级数)
注意到需要维护 \(\max\{dp[i][a_k]\}\),且 \(\max\) 的值随着 \(i\) 的下降单调不增。考虑双指针法。
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
ll n, m;
ll a[1000010];
vector<bool> cx;
vector<ll> dp;
vector<ll> tong;
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
int t; cin >> t;
while(t--) {
cin >> n >> m;
cl(cx, m + 10);
cl(tong, m + 10);
ll ans = inf;
ll mn = inf, mx = 0;
f(i, 1, n) {
cin >> a[i];
mn = min(mn, a[i]); mx = max(mx, a[i]);
cx[a[i]]=1;
}
cl(dp, m + 10);
f(i,0,mx)dp[i]=i;
f(i, 1,n) tong[a[i]] = 1;
int p = mx;
for(ll i = mx; i >= 1; i--) {
for(ll j = i * i; j <= mx; j += i) {
if(cx[j]) tong[dp[j]]--;
dp[j] = min(dp[j],dp[j/i]);
if(cx[j]) tong[dp[j]]++;
}
while(!tong[p]) p--;
if(i <= mn)
ans = min(ans, p - i);
}
cout << ans << endl;
}
return 0;
}
WLOI Round #2 C 归并排序
题意:给定一个序列 \(a\),可以做若干次如下操作:取两段相邻的有序区间,并花费 \(lmax \oplus rmax\) 的费用将这两个区间合并成一个有序区间。求将整个序列排序花费的费用最小值。
一眼看去是区间 DP。考虑 \(dp_{i, j} = \min \limits_{k = i} ^j \{dp_{i, k} + dp_{k + 1, j} + \max \limits_{x = i}^k a[x] \oplus \max \limits_{x = k+1}^j a[x]\}\)。
(为了方便,下文记 \(lmax = \max \limits_{x = i}^k a[x], rmax = \max \limits_{x = k+1}^j a[x]\)。)
但是这样做是错误的。看如下数据:
\(a = \{1,2,0,3,7\}\)
如果按照刚刚的方法,我们合并 \([1,2],[3,5]\) 的时候需要花费 \(2 \oplus 7\) 的代价。
但是发现可以直接合并 \([1,2],[3,4]\),同样可以把这个数列变有序。
关键问题在于,\(lmax=2\),而右区间内所有的 \(>lmax\) 的数都不需要参与合并,起到的作用只是减小代价。
更换 DP 策略:合并区间 \([i,j]\) 的时候,对于一个固定的 \(k\),记 \(l\) 为 \([k+1,j]\) 中下标最小的(第一个)满足 \(a_x > lmax\) 的 \(x\)。
那么如果 \(l = k+1\),这两个区间已经合并成功,\(dp_{i,j} = \min \limits_{k = i} ^j \{dp_{i, k} + dp_{k + 1, j}\}\)。
否则有 \(dp_{i, j} = \min \limits_{k = i} ^j \min \limits_{y = l-1} ^j \{dp_{i, k} + dp_{k + 1, j} + \max \limits_{x = i}^k a[x] \oplus \max \limits_{x = k+1}^y a[x]\}\)
这样我们得到了一个 \(O(n^4)\) 的 DP。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3;
typedef long long ll;
ll dp[N][N],a[N],mx[N][N],n;
int main()
{
// freopen("merge3.in", "r", stdin);
cin >> n;
for (int i = 1; i <= n;i++){
cin >> a[i];
}
for (int i = 1; i <= n;i++){
for (int j = i; j <= n;j++){
dp[i][j] = INT64_MAX;
}
}
for (int i = 1; i <= n;i++)
mx[i][i] = a[i];
for (int i = 1; i <= n; i++)
{
for (int j = i + 1; j <= n; j++)
{
mx[i][j] = max(mx[i][j-1], a[j]);
}
}
for (int i = 1; i <= n;i++)
dp[i][i] = 0;
for (int i = 1; i <= n;i++){
for (int j = i; j <= n;j++){
int flag = 0;
for (int k = i+1; k <= j; k++)
{
if(a[k]<a[k-1]){
flag = 1;
break;
}
}
if(flag==0)
dp[i][j] = 0;
}
}
for (int len = 2; len <= n; len++)
{
for (int i = 1; i <= n; i++)
{
int j = i + len - 1; //枚举区间[i,j],长度为len
if (j > n)
break;
for (int k = i; k + 1 <= j; k++) //[i,j]->[i,k][k+1,j]
{ // dp[i][j]=min(dp[i][k]+dp[k+1][j]+cost)
ll lmax = mx[i][k];
//枚举[k+1,j]找<lmax的最大数tmp
//如果[k+1,j]中没有<lmax的数,说明都大于等于lmax,因此右半边排好就行,不需要和左半边做合并了。
//此时dp[i][j]=dp[i][k]+dp[k+1][j]+0
ll tmp = -1;
for (int t = k + 1; t <= j; t++)
{
if (a[t] < lmax && a[t] > tmp)
tmp = a[t];
}
if(tmp==-1){
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
continue;
}
//走到这里说明[k+1,j]中有<lmax的数,这个数值大小就是tmp
//合并的代价至少是tmp^lmax
ll mnxor = lmax^tmp; //记录[k+1,j]中>=tmp的数与lmax异或的最小值
for (int t = k + 1; t <= j; t++)
{
//枚举[k+1,j]找>=tmp的a[t],使得与lmax异或值最小
if (a[t] >= tmp && (a[t] ^ lmax) < mnxor)
{
mnxor = a[t] ^ lmax;
}
}
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + mnxor);
}
}
}
cout << dp[1][n];
return 0;
}
考虑优化:
用类似双指针的做法将一个平方转移优化到线性转移:倒序枚举 \(i\),先枚举 \(k\),并维护 \(lmax\)(\(k \rightarrow k+1\) 时 \(O(1)\) 转移),然后第三层循环枚举 \(j\),此时 \(lmax\) 固定,维护 \(l\)(\(j \rightarrow j+1\) 的时候 \(O(1)\) 转移),并更新 \(dp_{i,j}\)。
时间复杂度 \(O(n^3)\)。
#include<bits/stdc++.h>
#define N 808
using namespace std;
int a[N];
long long dp[N][N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
if(j>i&&a[j]<a[j-1])break;
dp[i][j] = 0;
}
}
for(int i=n;i;i--){
int lmax = 0;
for(int k=i;k<=n;k++){
lmax = max(lmax,a[k]);
int cost = 2e9, mx = 0;
for(int j=k+1;j<=n;j++){
if(a[j]<lmax) mx = max(mx,a[j]);
else cost = min(cost, lmax^a[j]);
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]+min(cost,lmax^mx));
}
}
}
cout<<dp[1][n]<<endl;
}