dp决策单调性的学习

可供参考学习博客: link
link
link

四边形不等式

  对于这样的转移方程: f ( i ) = m i n { g ( j ) + w ( i , j ) }   ( 0 < i < x ) f(i) = min \{g(j) + w(i, j)\} \ (0 < i < x) f(i)=min{g(j)+w(i,j)} (0ix),直接转移的话复杂度是 O ( n 2 ) O(n^2) O(n2) 的。但是如果满足四边形不等式,就可以用决策单调性就行优化
  
  证明:
  假设 i i i 的决策点是 p [ i ] p[i] p[i],那么对于 ∀ j ∈ ( 0 , p [ i ] ) ∀j ∈ (0, p[i]) j(0,p[i]),有 g [ p [ i ] ] + w ( i , p [ i ] ) ≤ g [ j ] + w ( i , j )    ① g[p[i]] + w(i, p[i]) ≤ g[j] + w(i, j) \ \ ① g[p[i]]+w(i,p[i])g[j]+w(i,j)  
  对于 ∀ i ′ ∈ ( i , n ] ∀i' ∈ (i, n] i(i,n],由于 j < p [ i ] < i < i ′ j< p[i]<i <i' jp[i]ii,有 w ( i , j ) + w ( i ′ , p [ i ] ) ≤ w ( i ′ , j ) + w ( i , p [ i ] )    ② w(i, j) + w(i', p[i]) ≤ w(i', j) + w(i, p[i]) \ \ ② w(i,j)+w(i,p[i])w(i,j)+w(i,p[i])  
  对于②式即可以得到 w ( i ′ , p [ i ] ) − w ( i , p [ i ] ) ≤ w ( i ′ , j ) − w ( i , j ) w(i', p[i]) - w(i, p[i]) ≤ w(i', j) - w(i, j) w(i,p[i])w(i,p[i])w(i,j)w(i,j)
  ① + ③ 即可得到 g [ p [ i ] ] + w ( i ′ , p [ i ] ) ≤ g [ j ] + w ( i ′ , j ) g[p[i]] + w(i', p[i]) ≤ g[j] + w(i', j) g[p[i]]+w(i,p[i])g[j]+w(i,j),满足决策单调性

  同理,如果是 f ( i ) = m a x { g ( j ) + w ( i , j ) }   ( 0 < i < x ) f(i) = max \{g(j) + w(i, j)\} \ (0 < i < x) f(i)=max{g(j)+w(i,j)} (0ix), 需要满足 a < b < c < d ,    w ( d , a ) + w ( c , b ) ≤ w ( c , a ) + w ( d , b ) a < b < c<d, \ \ w(d, a) + w(c,b) ≤ w(c, a) + w(d, b) abcd,  w(d,a)+w(c,b)w(c,a)+w(d,b)
  
  
  例题 link,让你求 p i = m a x ( a j + ∣ i − j ∣ )    ( 1 ≤ j ≤ n ) p_i = max(a_j + \sqrt{|i - j|}) \ \ (1 ≤ j ≤ n) pi=max(aj+ij )  (1jn)
  经过化简, p i = m a x ( m a x 1 ≤ j ≤ i ( a j + i − j ) , m a x i ≤ j ≤ n ( a j + j − i ) ) p_i = max( max_{1 ≤ j ≤ i}(a_j + \sqrt{i - j}), max_{i ≤ j ≤ n}(a_j + \sqrt{j - i})) pi=max(max1ji(aj+ij ),maxijn(aj+ji )),后半部分翻转一下就行了,所以问题划归成 p i = m a x 1 ≤ j ≤ i ( a j + ∣ i − j ∣ )    ( 1 ≤ j ≤ n ) p_i = max_{1 ≤ j ≤ i}(a_j + \sqrt{|i - j|}) \ \ (1 ≤ j ≤ n) pi=max1ji(aj+ij )  (1jn)
  对于 i > j i > j i>j,若记 w [ i , j ] = i − j w[i, j] = \sqrt{i-j} w[i,j]=ij ,很容易可以证明 w ( i , j ) + w ( i + 1 , j + 1 ) ≥ w ( i , j + 1 ) , w ( i + 1 , j ) w(i, j) + w(i + 1, j + 1) ≥ w(i, j + 1), w(i + 1, j) w(i,j)+w(i+1,j+1)w(i,j+1),w(i+1,j)
  经过扩展可以得到 a < b < c < d ,    w ( d , a ) + w ( c , b ) ≤ w ( c , a ) + w ( d , b ) a < b < c<d, \ \ w(d, a) + w(c,b) ≤ w(c, a) + w(d, b) abcd,  w(d,a)+w(c,b)w(c,a)+w(d,b),满足四边形不等式

  

二分栈

  在满足四边形不等式之后,就可以用决策单调性来优化,一种比较常见的手法是二分栈每次得到了一个新的决策点,去更新当前这个决策点的最优区间(具体流程可以看上面那几个参考连接),可以做到 O ( n l o g n ) O(nlogn) O(nlogn) 的复杂度。
  
  但是二分栈也有它的劣势,就是我每得到一个新的决策点,我就能算出之后的 w 值,但是有些情况下是算不出来的,这时候需要分治。
  这一题先用二分栈的模板解决。
  

#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 5e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;

struct node
{
    int l, r, p;
} q[maxn];

int a[maxn], head, tail, n;
double dp[maxn], sq[maxn];

double W(int i, int j)        //w[i, j],i > j,这里的 w 值直接看成整个转移式了
{
    return a[j] + sq[i - j];
}

int Binary(int t,int x) {                  //对栈里的第 i 个三元组进行二分
	int ans = q[t].r + 1, l = q[t].l, r = q[t].r;          
	while(l <= r)
    {
		int mid=(l + r) / 2;
		if(W(mid, q[t].p) <= W(mid, x)) ans = mid,r = mid - 1;        //求的是最大值
		else l = mid + 1;
	}
	return ans;
}

void Insert(int i)
{
    q[tail].l = max(q[tail].l, i);              //更新边界
    while(head <= tail && W(q[tail].l, i) >= W(q[tail].l, q[tail].p)) tail--;         //求的是最大值
    if(head > tail)
    {
        q[++tail].l = i;
        q[tail].r = n;
        q[tail].p = i;
    }
    else
    {
        int pos = Binary(tail, i);
        if(pos > n) return;
        q[tail].r = pos - 1;
        q[++tail].l = pos;
        q[tail].r = n;
        q[tail].p = i;
    }
}

void solve()
{
    head = 1, tail = 0;                //初始情况栈为空
    for (int i = 1; i <= n; i++)
    {
        Insert(i);                           //插入第 i 个决策点

        if(head <= tail && q[head].r < i)  head++;         //更新右指针
        else q[head].l = i;                //更新边界        
        dp[i] = max(dp[i], W(i, q[head].p));            //给第 i 个点做决策
    }
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        sq[i] = sqrt(i);
    }
    solve();
    for (int i = 1; i <= n / 2; i++)  swap(a[i], a[n+1-i]),  swap(dp[i], dp[n+1-i]);
    solve();

    for (int i = n; i >= 1; i--)  printf("%d\n", (int)ceil(dp[i]) - a[i]);
}

  

分治

  使用二分栈的时候有一个缺点,就是 w ( i , j ) w(i, j) w(i,j) 需要很快的算出来,但是在有些情况下是不好算的。这个时候如果被决策点在将来不会成为决策点,变可以用分治的思想去维护。
  
   例题:link 给定一个序列 a,要把它分成 k 个子段。每个子段的费用是其中相同元素的对数。求所有子段的费用之和的最小值。(k ≤ 20)
  可以让 d p [ j ] [ i ] dp[j][i] dp[j][i] 表示将前 i i i 个数分成了 j j j 段的最小费用,那么我们可以得到 d p [ j ] [ i ] = d p [ j − 1 ] [ k ] + w ( k + 1 , i ) dp[j][i] = dp[j-1][k] + w(k+1, i) dp[j][i]=dp[j1][k]+w(k+1,i),其中 w ( k + 1 , i ) w(k+1, i) w(k+1,i) 表示 [ k + 1 , i ] [k+1, i] [k+1,i] 为一段的费用。
  这个 dp 可以证出是符合决策单调性的,但是不能 O ( 1 ) O(1) O(1) 的计算 w w w 值,所以这里可以用到分治。
  分治的时候给定一段决策点区间和一段被决策点区间,然后计算被决策点mid 的值,计算方法是遍历决策点,所以需要 O ( n ) O(n) O(n)解决。
  在这题中 w w w 值如果每个拆开算,每个需要 O ( n ) O(n) O(n),总复杂度就是 O ( n 2 ) O(n^2) O(n2),但是如果用莫队的思想进行维护的话, 复杂度是 O ( n ) O(n) O(n)。具体细节在代码中。

#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 5e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;

ll dp[21][maxn], ans;
int cnt[maxn], a[maxn], k, L, R, cur, n;

ll cal(int l, int r)                 //计算w[l, r]
{
    while(L < l)  ans -= cnt[a[L]] - 1, cnt[a[L]]--, L++;
    while(L > l)  L--, ans += cnt[a[L]], cnt[a[L]]++;
    while(R > r)  ans -= cnt[a[R]] - 1, cnt[a[R]]--, R--;
    while(R < r)  R++, ans += cnt[a[R]], cnt[a[R]]++;
    return ans;
}

void solve(int dl, int dr, int l, int r)        //dl, dr为决策点,l,r为被决策的区间
{
    if(dl > dr || l > r) return;
    int mid = (l + r) / 2;

    int p;
    ll res = 1e18, tmp;            
    for (int i = dl; i <= dr; i++)            //给mid暴力计算出w值
    {
        tmp = cal(i + 1, mid);                 //处理左儿子的时候只动L,左儿子到右儿子移动L,R,右儿子继续移动L
        if(res > dp[cur-1][i] + tmp)
            res = dp[cur-1][i] + tmp, p = i;
    }
    dp[cur][mid] = res;
    solve(dl, p, l, mid - 1);             //分治
    solve(p, dr, mid + 1, r);
}

int main()
{
    scanf("%d %d", &n, &k);
    for (int i = 1; i <= n; i++)  scanf("%d", &a[i]);
    cnt[a[1]]++;          //先指定一个L,R
    L = R = 1;

    cur = 1;
    solve(0, 0, 1, n);
    for (cur = 2; cur <= k; cur++)  solve(cur - 1, n, cur, n);      
    printf("%lld\n", dp[k][n]);
}

  link
这题也是和上一题一个套路,几乎一样的转移式子,就稍微改一下就行,但是复杂度是 O ( n k l o g n ) O(nklogn) O(nklogn) 的,用快读才会刚刚卡过,更好的做法是 wqs 二分。(待补)
要注意的是,莫队移动指针的时候可能有 l > r的情况(即有人坐不上船),这时候记得直接返回0。

#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 4010;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;

int w[maxn][maxn], sum[maxn][maxn];
int dp[802][maxn], L, R, n, k, cur, ans;

inline int read()
{
        int x=0,f=1;char ch=getchar();
        while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
        while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
        return x*f;
}

int cal(int l, int r)                 //计算w[l, r]
{
    if(l > r)  return 0;
    while(L < l)  ans -= sum[L][R], L++;
    while(L > l)  L--, ans += sum[L][R];
    while(R > r)  ans -= sum[R][L], R--;
    while(R < r)  R++, ans += sum[R][L];
    return ans;
}

void solve(int dl, int dr, int l, int r)        //dl, dr为决策点,l,r为被决策的区间
{
    if(dl > dr || l > r) return;
    int mid = (l + r) / 2;

    int p;
    int res = INF, tmp;            
    for (int i = dl; i <= dr; i++)            //给mid暴力计算出w值
    {
        tmp = cal(i + 1, mid);                 //处理左儿子的时候只动L,左儿子到右儿子移动L,R,右儿子继续移动L
        if(res > dp[cur-1][i] + tmp)
            res = dp[cur-1][i] + tmp, p = i;
    }
    dp[cur][mid] = res;
    solve(dl, p, l, mid - 1);             //分治
    solve(p, dr, mid + 1, r);
}

int main()
{
    n = read();   k = read();
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            w[i][j] = read();
    for (int i = 1; i <= n; i++)
    {
        for (int j = i + 1; j <= n; j++)
            sum[i][j] = sum[i][j - 1] + w[i][j];
        for (int j = i - 1; j >= 0; j--)
            sum[i][j] = sum[i][j + 1] + w[i][j];
    }

    L = R = 1;
    for (cur = 1; cur <= k; cur++)
    {
        if(cur == 1)  solve(0, 0, cur, n);
        else solve(cur - 1, n, cur, n);
    }
    printf("%d\n", dp[k][n]);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值