双指针

也叫尺取法,当两个变量的关系是当 \(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;
}
posted @ 2022-07-08 10:37  OIer某罗  阅读(16)  评论(0编辑  收藏  举报