0
点赞
收藏
分享

微信扫一扫

Java执行Linux命令并等待结果返回

GhostInMatrix 2023-06-01 阅读 66

文章目录

单调队列优化dp

文章首发于我的个人博客:欢迎大佬们来逛逛

单调队列优化dp的建模形式:这是窗口右滑动的情况

对于窗口左滑动的也是同理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6b43a5LW-1685532776854)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ce3nZm5j-1685532776854)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled%201.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDlxFfpM-1685532776855)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled%202.png)]

烽火传递

例如:烽火台及其代价如下所示:

1 2 5 6 2 ,其中k=3

我们必须选择两个:

  1. 1 和 5:代价为6
  2. 1 和 6:代价为7
  3. ……
  4. 2 和 2:代价为4,可以得知这是最优解。

因此我们定义 d p [ i ] dp[i] dp[i] 数组 表示当前位置的最小代价值,因此最后的结果就是 d p [ n ] dp[n] dp[n]

其中状态转移可以由前一些位置的最小的值来转移得到:

d p [ i ] = m i n ( d p [ j ] ) + w [ i ] dp[i]=min(dp[j])+w[i] dp[i]=min(dp[j])+w[i]

其中 d p [ j ] dp[j] dp[j] 中的 j 的位置我们可以根据 k k k 的值来确定:

j ∈ [ i − k , i − 1 ] j \in \left [i-k,i-1 \right ] j[ik,i1]

这意味如果我们点燃位置为 i i i 的烽火台,则必须满足在 [ i − k , i − 1 ] \left [i-k,i-1 \right ] [ik,i1] 位置至少点燃一个烽火台:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EiRqpji2-1685532776855)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled%203.png)]

因此我们枚举 i i i 的位置,对于每一个位置通过单调队列维护前面区间内一个最小值,然后根据上面的状态转移方程转移即可。

#include<bits/stdc++.h>
#if 0
    #define int long long
#endif

const int N=1e6+10;
int n,k;
int w[N],dp[N];
std::deque<int> deq;
signed main(){  
    std::cin>>n>>k;
    for (int i=1;i<=n;i++){
        std::cin>>w[i];
    }   
    int ans=1e9;
    for (int i=1;i<=n;i++){
        if (!deq.empty() && deq.front()<i-k){
            deq.pop_front();
        }
        //维护队列最小值
        while (!deq.empty() && dp[i-1]<=dp[deq.back()]){
            deq.pop_back();
        }
        deq.push_back(i-1);
        dp[i]=dp[deq.front()]+w[i];
        if (i>=n-k+1){
            ans=std::min(ans,dp[i]);
        }
    }
    std::cout<<ans;
    return 0;
}

修剪草坪

示例给出了:

1 2 3 4 5 ,其中k=2

最优解是选择1 24 5两个子序列,结果为 12

根据贪心的思想,如果我们想要使得最后的和最大, 应该尽量往长了选,则我们假设选择了n个长度为 k 的子序列,则第 k+1 个数字一定不会选,因此我们就可以对这个不会选的数字进行操作。

与其让我们求n个 长度为k 的子序列的和的最大值,不如求在 n个 长度为k+1 的子序列中选择最小值,然后计算他们的和,最后直接用整个数列的和减去由若干最小值组成的和即可

因此状态转移方程如下:

d p [ i ] = m i n ( d p [ j ] ) + w [ i ] dp[i]=min(dp[j]) +w[i] dp[i]=min(dp[j])+w[i]

注意 d p dp dp 数组表示的是在 k+1 长度中选择的最小值

其中 j j j 的取值范围是:

j ∈ [ i − k , i − 1 ] j \in \left [i-k,i-1 \right ] j[ik,i1]

#include<bits/stdc++.h>
#if 1
    #define int long long
#endif

const int N=1e6+10;
int n,k;
int w[N],dp[N];
std::deque<int> deq;
signed main(){  
    std::cin>>n>>k;
    for (int i=1;i<=n;i++){
        std::cin>>w[i];
    }   
    k+=1;
    //选择子序列,并且使得数列中没有连续的超过k个数被选,使得选择的数之和最大
    //选择不超过连续k个数字之和的最大值 ==> sum-选择k+1个数字组成若干个最小值
    int sum=1e18;
    for (int i=1;i<=n;i++){
        if (!deq.empty() && deq.front()<i-k){
            deq.pop_front();
        }
        while (!deq.empty() && dp[deq.back()]>=dp[i-1]){
            deq.pop_back();
        }
        deq.push_back(i-1);
        dp[i]=dp[deq.front()]+w[i];
        if (i>=n-k+1){
						//获得最小值
            sum=std::min(sum,dp[i]);
        }
    }
    std::cout<<std::accumulate(w+1,w+1+n,0ll)-sum;
    return 0;
}

绿色通道

绿色通道 - 洛谷

根据题目描述的最大值最小,我们很容易想到是二分答案

同时题目告诉你了最长的空题目段越长,马老师越生气,这就是相当于满足了单调性。

因此我们可以二分查找 空题目段的长度,然后选择套用二分查找求最小值的模板即可。

其中我们在二分查找的过程中,假设当前的空题目段长度为 k, 则第 k+1 题目一定不是,因此我们就可以像上一道题目那样,单调队列维护区间最小值。

注意到我们二分的是 k+1 的长度,因此最后我们还需要 -1

#include<bits/stdc++.h>
#if 1
    #define int long long
#endif

const int N=5e4+10;
int dp[N],w[N];
int n,t;
bool check(int mid){
    //mid表示选择的空题目段长+1 
    std::deque<int> deq;
    memset(dp,0,sizeof(dp));
    for (int i=1;i<=n;i++){
        if (!deq.empty() && deq.front()<i-mid){
            deq.pop_front();
        }
        while (!deq.empty() && dp[deq.back()]>=dp[i-1]){
            deq.pop_back();
        }
        deq.push_back(i-1);
        dp[i]=dp[deq.front()]+w[i];
        if (i>=n-mid+1 && dp[i]<=t){
            //mid越大,表示选择的空题目段越长,则能做的题目就越少,因此不超过t分钟的条件就越容易满足,此时返回true,更新最小值
            return true;
        }
    }
    return false;
}
signed main(){
    std::cin>>n>>t;
    for (int i=1;i<=n;i++){
        std::cin>>w[i];
    }
    //最大值最小:使得最长的空题段最短是多少
    //二分选择的 空题段长+1
    int l=-1,r=n+1;
    while (l+1<r){
        int mid=l+r>>1;
        if (check(mid)){
            r=mid;
        }
        else{
            l=mid;
        }
    }
    std::cout<<r-1;
    return 0;
}

琪露诺

琪露诺 - 洛谷

很容易可以看出来这道题目是给定区间的单调队列优化 的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8gykVs2O-1685532776856)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled%204.png)]

状态转移方程与前面都是一样的。

#include<bits/stdc++.h>
#if 0
    #define int long long
#endif
const int N=2e5+10;
int n,l,r;
int w[N],dp[N];
std::deque<int> deq;
signed main(){
    std::cin>>n>>l>>r;
    for (int i=0;i<=n+1;i++){
        std::cin>>w[i];
    }
    //i可以跳跃到[i+l,i+r]中的任意位置
    //同理i位置可以由[i-r,i-l]区间中任意位置跳跃过来,维护窗口中的最大值即可
    memset(dp,-0x3f,sizeof(dp));
    dp[0]=0;
    int ans=-1e9;
    //注意从l开始,因为一开始只能跳到[l,r]的位置
    for (int i=l;i<=n;i++){
        if (!deq.empty() && deq.front()<i-r){
            deq.pop_front();
        }
        //维护单调队列最大值
        while (!deq.empty() && dp[deq.back()]<=dp[i-l]){
            deq.pop_back();
        }
        deq.push_back(i-l);
        dp[i]=dp[deq.front()]+w[i];
        if (i>=n-r+1){
            ans=std::max(ans,dp[i]); //在最后的区间内累加答案
        }
    }
    std::cout<<ans;
    return 0;
}

旅行问题

我们定义 s u m [ i ] sum[i] sum[i] 表示为从 i 号车站出发到下一站剩余的流量:

s u m [ i ] = s u m [ i + n ] = o i l [ i ] − d i s [ i ] sum[i] = sum[i+n] =oil[i] -dis[i] sum[i]=sum[i+n]=oil[i]dis[i]

对于环形的问题,我们常常断环成链。

然后计算他们的前缀和:

s u m [ i ] + = s u m [ i − 1 ] sum[i] +=sum[i-1] sum[i]+=sum[i1]

对于判断 i 号点能否绕一圈,我们可以用

s u m [ j ] − s u m [ i − 1 ] > = 0 sum[j]-sum[i-1]>=0 sum[j]sum[i1]>=0

来判断,如果此值大于等于0,则说明 i 号点可以绕一圈,对于 j 我们可以单调队列维护一个最小的 j

同时判断逆时针能够绕一圈,我们可以维护一个后缀和,然后执行相反的操作即可:

s u m [ j ] − s u m [ i + 1 ] > = 0 sum[j]-sum[i+1]>=0 sum[j]sum[i+1]>=0

#include<bits/stdc++.h>
#if 1
    #define int long long
#endif

const int N=1e6+10;
int n;
int oil[N],dis[N];
int sum[N<<2],fg[N<<2]; //sum[i]表示从i点出发顺时针或者逆时针到达各个点的剩余油量
std::deque<int> deq;
signed main(){
    std::cin>>n;
    for (int i=1;i<=n;i++){
        std::cin>>oil[i]>>dis[i];
    }

    //顺时针
    for (int i=1;i<=n;i++){
        sum[i]=sum[i+n]=oil[i]-dis[i];
    }
    for (int i=1;i<=n*2;i++){
        sum[i]+=sum[i-1];
    }
    //窗口往左滑动
    for (int i=n*2;i>=1;i--){
        if (!deq.empty() && deq.front()>i+n-1){
            deq.pop_front();
        }
        //维护最小的sum[j],如果sum[j]-sum[i-1]=0则满足i点可以绕一圈
        while (!deq.empty() && sum[deq.back()]>=sum[i]){
            deq.pop_back();
        }
        deq.push_back(i);
        if (i<=n && sum[deq.front()]-sum[i-1]>=0){
            fg[i]=true;
        }
    }
    
    memset(sum,0,sizeof(sum));
    deq.clear();
    //逆时针
    dis[0]=dis[n];
    for (int i=n;i>=1;i--){
        sum[i]=sum[i+n]=oil[i]-dis[i-1];
    }
    for (int i=n*2;i>=1;i--){
        sum[i]+=sum[i+1];
    }
    //窗口往右边滑动
    for (int i=1;i<=n*2;i++){
        if (!deq.empty() && deq.front()<i-n+1){
            deq.pop_front();
        }
        while (!deq.empty() && sum[deq.back()]>=sum[i]){
            deq.pop_back();
        }
        deq.push_back(i);
        if (i>=n+1 && sum[deq.front()]-sum[i+1]>=0){
            //注意此时的序号为i-n
            fg[i-n]=true;
        }
    }
    for (int i=1;i<=n;i++){
        if (fg[i]){
            std::cout<<"TAK\n";
        }
        else{
            std::cout<<"NIE\n";
        }
    }
    return 0;
}

Watching Fireworks is Fun

Watching Fireworks is Fun - 洛谷

注意到从一个位置到另一个位置可以通过两种方式得到:

窗口向右滑动:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvmx4IpO-1685532776856)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled%205.png)]

窗口向左滑动:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eeg75d6t-1685532776857)(%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E4%BC%98%E5%8C%96dp%20dd9f82e1dca24b29ac9925e1303dfacb/Untitled%206.png)]

我们定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示当前放的是第 i 次烟花,位置为 j 时的最大权值。

状态转移方程如下:

d p [ n o w ] [ i ] = d p [ p r e ] [ j ] + b [ i ] − a b s ( a [ i ] − x ) dp[now][i]=dp[pre][j]+ b[i]-abs(a[i]-x) dp[now][i]=dp[pre][j]+b[i]abs(a[i]x)

可以利用滚动数组优化,把数组将为二维。

#include<bits/stdc++.h>
#if 1
    #define int long long
#endif

const int N=150010;
int dp[2][N]; //滚动数组: dp[i][j]表示放第i个烟花,当前位置位于j时的最大快乐值
int n,m,d;
int a[N],b[N],t[N];
signed main(){
    std::cin>>n>>m>>d;
    for (int i=1;i<=m;i++){
        std::cin>>a[i]>>b[i]>>t[i];
    }
    memset(dp,-0x3f,sizeof(dp));
    for (int i=1;i<=n;i++){
        dp[0][i]=0;  //初始化,放第0个烟花,位置在任何时刻都是0快乐度
    }
    //遍历烟花
    for (int z=1;z<=m;z++){
        //now表示当前,pre表示上一个
        int now=z&1,pre=(z-1)&1;
        std::deque<int> deq;
        //遍历位置:窗口往右滑动
        for (int i=1;i<=n;i++){
            if (!deq.empty() && deq.front()<i-d*(t[z]-t[z-1])){
                deq.pop_front();
            }
            //维护队列最大值
            while (!deq.empty() && dp[pre][deq.back()]<=dp[pre][i]){
                deq.pop_back();
            }
            deq.push_back(i);
            dp[now][i]=dp[pre][deq.front()]+b[z]-abs(a[z]-i);
        }
        //窗口往左滑动
        for (int i=n;i>=1;i--){
            if (!deq.empty() && deq.front()>i+d*(t[z]-t[z-1])){
                deq.pop_front();
            }
            while (!deq.empty() && dp[pre][deq.back()]<=dp[pre][i]){
                deq.pop_back();
            }
            deq.push_back(i);
            dp[now][i]=std::max(dp[now][i],dp[pre][deq.front()]+b[z]-abs(a[z]-i));
        }
    }
    int ans=-1e18;
    //最后一次在m&1,位置为j时枚举一个最大值
    for (int i=1;i<=n;i++){
        ans=std::max(dp[m&1][i],ans);
    }
    std::cout<<ans;
    return 0;   
} 

瑰丽华尔兹

[NOI2005] 瑰丽华尔兹 - 洛谷

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示当前位置为 ( i , j ) (i,j) (i,j) 时的最大移动距离

我们首先需要枚举四个方向,然后进行求解。

我们先来看往右移动的话:

很容易想到状态转移方程:

d p [ i ] [ j ] = m a x ( d p [ i ] [ k ] + j − k ) dp[i][j]=max(dp[i][k]+j-k) dp[i][j]=max(dp[i][k]+jk)

即当前点的最大移动距离就等于 ( i , k ) (i,k) (i,k) 点的距离 + ( j − k ) (j-k ) (jk) 的距离。

因此我们只需要维护一个 d p [ i ] [ k ] + j − k dp[i][k] +j-k dp[i][k]+jk 的最大值,我们可以使用单调队列进行维护

这是向右移动的情况,再考虑其他三种情况也是同样的道理。

但是发现一个问题,往右移动, i 是不变的,但是往下移动的时候,j 又是不变的,我们很难用 d p [ i ] [ k ] dp[i][k] dp[i][k]具体的表示四个方向,因此我们抽象为:

d p [ i ] [ j ] = m a x ( f + j − p o s ) dp[i][j]=max(f+j-pos) dp[i][j]=max(f+jpos)

其中 f f f 就是转移之前的最大移动距离, p o s pos pos 表示的就是转移之前的那个位置。

因此我们对于四种方向的枚举就可以实现了:

for (int i=1;i<=k;i++){
        int s,t,d;  //时间区间[s,t] d方向 1234 上下左右
        std::cin>>s>>t>>d; 
        int time=t-s+1;
        if (d==1){//上
            for (int i=1;i<=m;i++){
                get(n,i,n,time,d);
            }
        }
        if (d==2){//下
            for (int i=1;i<=m;i++){
                get(1,i,n,time,d);
            }
        }
        if (d==3){//左
            for (int i=1;i<=n;i++){
                get(i,m,m,time,d);
            }
        }
        if (d==4){//右
            for (int i=1;i<=n;i++){
                get(i,1,m,time,d);
            }
        }
    }

其中 g e t get get 函数就是我们维护单个方向滑动窗口最大值的函数:

  • x和y:每个方向的起始位置
  • L:表示最多在这个方向移动L距离
  • time:表示时间间隔。
  • d:表示方向,用于移动。
void get(int x,int y,int L,int time,int d);

我们在此函数中利用滑动窗口维护一个最大值即可,注意当碰到障碍的时候,我们的窗口清零。

#include<bits/stdc++.h>
#if 0
    #define int long long
#endif

const int N=210;
int n,m,x,y,k,ans;
char map[N][N];
int dp[N][N]; //dp[i][j]表示在(i,j)位置的最大价值
int dx[5]{0,-1,1,0,0},dy[5]{0,0,0,-1,1};
struct Node{
    int pos,dp; //位置和最长距离
};
void get(int x,int y,int L,int time,int d){
    //(x,y)表示起始位置,L表示当前方向最长移动长度,time表示时间长度
    std::deque<Node> deq;
    //注意!!! 这个地方要从(x,y)开始,所有一开始不能把x+=,y+=放在下面,因此要放在for循环的后面
    for (int i=1;i<=L;i++,x+=dx[d],y+=dy[d]){ //朝着方向d一直移动
        if (map[x][y]=='x'){
            deq.clear();
        }
        else{
            //单调队列维护最大值
            if (!deq.empty() && deq.front().pos<i-time){
                deq.pop_front();
            }
            //维护窗口最大值
            while (!deq.empty() && deq.back().dp+i-deq.back().pos<=dp[x][y]){
                deq.pop_back();
            }
            deq.push_back({i,dp[x][y]});
            dp[x][y]=deq.front().dp+i-deq.front().pos;
            ans=std::max(ans,dp[x][y]);
        }
    }
}
signed main(){
    std::cin>>n>>m>>x>>y>>k;
    for (int i=1;i<=n;i++){
        for (int j=1;j<=m
        ;j++){
            std::cin>>map[i][j];
        }
    }
    memset(dp,-0x3f,sizeof(dp));
    dp[x][y]=0;
    for (int i=1;i<=k;i++){
        int s,t,d;  //时间区间[s,t] d方向 1234 上下左右
        std::cin>>s>>t>>d; 
        int time=t-s+1;
        if (d==1){//上
            for (int i=1;i<=m;i++){
                get(n,i,n,time,d);
            }
        }
        if (d==2){//下
            for (int i=1;i<=m;i++){
                get(1,i,n,time,d);
            }
        }
        if (d==3){//左
            for (int i=1;i<=n;i++){
                get(i,m,m,time,d);
            }
        }
        if (d==4){//右
            for (int i=1;i<=n;i++){
                get(i,1,m,time,d);
            }
        }
    }
    std::cout<<ans;
    return 0;
}

股票交易

[SCOI2010]股票交易 - 洛谷

定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示第 i i i 天持股 j j j 数量的最大利润

我们可以分成四种情况讨论:

  • 只买入: d p [ i ] [ j ] = − a s ⋅ j dp[i][j]=-as\cdot j dp[i][j]=asj
  • 不买不卖: d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ j ] ) dp[i][j]=max(dp[i][j],dp[i-1][j]) dp[i][j]=max(dp[i][j],dp[i1][j])
  • 在之前某天的基础上买入 d p [ i ] [ j ] = m a x ( d p [ i − w − 1 ] [ k ] − a p ∗ ( j − k ) ) dp[i][j]=max(dp[i-w-1][k]-ap*(j-k)) dp[i][j]=max(dp[iw1][k]ap(jk))
    • 其中 k k k 的取值范围: k ∈ [ j − a s , j ] k\in [j-as,j] k[jas,j] 表示全买或者一个都不买
  • 在之前某天的基础上卖出 d p [ i ] [ j ] = m a x ( d p [ i − w − 1 ] [ k ] + b p ∗ ( k − j ) ) dp[i][j]=max(dp[i-w-1][k]+bp*(k-j)) dp[i][j]=max(dp[iw1][k]+bp(kj))
    • 其中 k k k 的取值范围: k ∈ [ j , j + b s ] k \in [j,j+bs] k[j,j+bs] 表示一个都不卖或者全卖

因此我们便可以使用单调队列维护窗口的最大值了。

其中 k k k 便是我们需要维护的值。

注意窗口的移动情况:

  • 买入:窗口往右滑,顺序遍历
  • 卖出:窗口往左滑,逆序遍历
#include<bits/stdc++.h>
#if 0
    #define int long long
#endif

const int N=5e3+10;
int t,maxp,w;
int ap,bp,as,bs;
int dp[N][N]; //dp[i][j]表示第i天持股j时的最大价值
signed main(){
    memset(dp,-0x3f,sizeof(dp));
    std::cin>>t>>maxp>>w; //t天,持股不能超过maxp,交易间隔w天
    for (int i=1;i<=t;i++){
        //买入价ap,卖出价bp,最多买入as,最多卖出bs
        std::cin>>ap>>bp>>as>>bs;   
        //凭空买入
        for (int j=0;j<=as;j++){
            dp[i][j]=-ap*j;
        }
        //不买不卖
        for (int j=0;j<=maxp;j++){
            dp[i][j]=std::max(dp[i][j],dp[i-1][j]);
        }
        if (i<=w){
            continue;
        }
        //买入
        std::deque<int> deq;
        for (int j=0;j<=maxp;j++){
            if (!deq.empty() && deq.front()<j-as){
                deq.pop_front();
            }
            while (!deq.empty() && dp[i-w-1][deq.back()]+deq.back()*ap-j*ap<=dp[i-w-1][j]){
                deq.pop_back();
            }       
            deq.push_back(j);
            dp[i][j]=std::max(dp[i][j],dp[i-w-1][deq.front()]-ap*(j-deq.front()));
        }
        deq.clear();
        //卖出
        for (int j=maxp;j>=0;j--){
            if (!deq.empty() && deq.front()>j+bs){
                deq.pop_front();
            }
            while (!deq.empty() && dp[i-w-1][deq.back()]+deq.back()*bp-j*bp<=dp[i-w-1][j]){
                deq.pop_back();
            }
            deq.push_back(j);
            dp[i][j]=std::max(dp[i][j],dp[i-w-1][deq.front()]+bp*(deq.front()-j));
        }
    }
    std::cout<<dp[t][0];
    return 0;
}
举报

相关推荐

0 条评论