最近遇到的各种DP 1.0

3 篇文章 0 订阅

最近遇到的各种DP 1.0

前言

全是codeforces上的,1900~2200左右吧,可能会一直更新这个专题。

CF82D Two out of Three (巧妙的转移)

题目链接

http://codeforces.com/problemset/problem/82/D

题意:

(来自洛谷)

一队顾客排在一位收银员前面。他采取这样一个策略:每次,假如队伍有至少两人,就会从前面的前三人(如果有)中选取两位一起收银,所花费的时间为这两人单独收银所需时间的最大值。如果只有两人,那么一起收银;如果只有一人,那么单独收银。请问所需的总时间最少是多少? 1 ≤ n ≤ 1000 , 1 ≤ a i ≤ 1 0 6 1 \le n \le 1000 , 1 \le a_i \le 10^6 1n1000,1ai106

Input

4
1 2 3 4

Output

6
1 2
3 4

(输出最少总时间和收银方式)

思路:

比较巧妙的动态转移。

每次在前3个人里选2个,手推一下可以发现,第 i i i轮收银开始时,前 2 ∗ i − 1 2*i-1 2i1号剩且仅剩一个人在队列中。

也就是说 , 第i轮收银的前三人编号一定为: j , 2 ∗ i , 2 ∗ i + 1 , j ϵ [ 1 , 2 ∗ i − 1 ] j , 2*i , 2*i+1 , j \epsilon [1,2*i-1] j,2i,2i+1,jϵ[1,2i1]

我们令 dp[i][j] 为 第 i 轮 第 j 个人的情况,可得转移方程:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ 2 ∗ i ] , t [ 2 ∗ i + 1 ] ) ) dp[i][j] = max( dp[i-1][j] , max(t[2*i],t[2*i+1]) ) dp[i][j]=max(dp[i1][j],max(t[2i],t[2i+1]))

这里得分三种情况讨论,因为是在 j , i ∗ 2 , i ∗ 2 + 1 j , i*2 , i*2+1 j,i2,i2+1 这三个人中选2个。

设 a = 2i , b = 2i+1

1.选 a,b

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ a ] , t [ b ] ) ) dp[i][j] = max(dp[i-1][j],max(t[a],t[b])) dp[i][j]=max(dp[i1][j],max(t[a],t[b]))

2.选 j,a

d p [ i ] [ b ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ j ] , t [ a ] ) ) dp[i][b] = max(dp[i-1][j],max(t[j],t[a])) dp[i][b]=max(dp[i1][j],max(t[j],t[a]))

3.选 j,b

d p [ i ] [ a ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ j ] , t [ b ] ) ) dp[i][a] = max(dp[i-1][j],max(t[j],t[b])) dp[i][a]=max(dp[i1][j],max(t[j],t[b]))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHH6PLWc-1647866683460)(https://github.com/xi-qaq/picture/blob/master/image-20220321134221743.png)]

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int dp[N][N];
struct node{
    int i;
    int j;
    int last;
};
node path[N][N];
int t[N];
int n;
int main(){
    ios::sync_with_stdio(false);
    cin>>n;
    int m = n;
    for(int i=1;i<=n;i++)
        cin>>t[i];
    t[++n] = 0;   // 最后补一个权值为0的人可以避免奇数特判
    memset(dp,0x3f3f3f3f,sizeof dp);
    dp[1][1] = max(t[2],t[3]),path[1][1] = {2,3,3};
    dp[1][2] = max(t[1],t[3]),path[1][2] = {1,3,3};
    dp[1][3] = max(t[1],t[2]),path[1][3] = {1,2,3};
    // 第1轮:1,2,3
    // 第2轮:1,2,3,4,5
    // 第i轮的候选人必为 : j , i*2 , i*2+1  (其中 1 <= j <= i*2-1 )
    for(int i=2;i<=n/2;i++){
        int a = i*2,b = i*2+1;
        for(int j=1;j<2*i;j++){
            if(dp[i-1][j]+max(t[a],t[b])<dp[i][j]){
                dp[i][j] = dp[i-1][j]+max(t[a],t[b]);
                path[i][j] = {a,b,j};   // path记录路径 : a,b是当前收银的两个人, j指从j继承过来
            }
            if(dp[i-1][j]+max(t[j],t[a])<dp[i][b]){
                dp[i][b] = dp[i-1][j]+max(t[j],t[a]);
                path[i][b] = {j,a,j};
            }
            if(dp[i-1][j]+max(t[j],t[b])<dp[i][a]){
                dp[i][a] = dp[i-1][j]+max(t[j],t[b]);
                path[i][a] = {j,b,j};
            }
        }
    }
    cout<<dp[n/2][n]<<endl;
    // 最后一个收银的一定是dp[n/2][n] , 然后通过上面存好的路径path.last一个一个找回去
    stack<pair<int,int>> st;
    int now = n;
    for(int i=n/2;i>=1;i--){
        st.push(make_pair(path[i][now].i,path[i][now].j));
        now = path[i][now].last;
    }
    while(!st.empty()){
        if(st.top().second>m){
            cout<<st.top().first<<endl;
        }
        else
            cout<<st.top().first<<" "<<st.top().second<<endl;
        st.pop();
    }
}

CF533B Work Group(树形dp)

题目链接:

http://codeforces.com/problemset/problem/533/B

题意:

公司有n个人,1是总裁,每个人有一个直接上司。每一个人有一个权值 a i a_i ai,要求找一个集合,使集合中所有人权值之和最大。其中每一个人的下属(直接,间接)总数都必须是偶数。输出最大权值。 1 ≤ n ≤ 2 ∗ 1 0 5 , 1 ≤ a i ≤ 1 0 5 1 \le n \le 2*10^5 , 1 \le a_i \le 10^5 1n2105,1ai105

Input

7
-1 3
1 2
1 1
1 4
4 5
4 3
5 2

Output

17

(输入第一行是n , 输入第i+1行表示第i个人的直接上司是x,它的权值是y)

思路:

蛮简单的一道树形dp,我却做了好久 23333.

一开始没看到间接下属也算 23333.

d p [ N ] [ 2 ] dp[N][2] dp[N][2] , 表示目前是第i个人,它的子树是 奇数/偶数 情况下的合法最优解。

我一开始感觉要再开一维 d p [ N ] [ 2 ] [ 2 ] dp[N][2][2] dp[N][2][2] , 第3维表示该节点选或不选,但其实没必要,因为在合法情况下奇数必选偶数必不选。

dp[u][0] = max(dp[v][0]+x0,dp[v][1]+x1);
dp[u][1] = max(dp[v][1]+x0,x1+dp[v][0]);

其中 x 0 = d p [ u ] [ 0 ] , x 1 = d p [ u ] [ 1 ] x_0 = dp[u][0] ,x_1 = dp[u][1] x0=dp[u][0],x1=dp[u][1]

代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+100;
struct  E{
    int to;
    int nxt;
}e[N<<1];
int head[N],tot;
int dp[N][2];
int a[N];
void add_edge(int u,int v){
    e[++tot].nxt = head[u];
    e[tot].to = v;
    head[u] = tot;
}
int n;
void dfs(int u,int f){
	dp[u][1] = -0x3f3f3f3f;
    for(int i=head[u];i;i=e[i].nxt){
        int v = e[i].to;
        if(v==f) continue;
        dfs(v,u);
        int x0 = dp[u][0],x1 = dp[u][1];
       dp[u][0] = max(dp[v][0]+x0,dp[v][1]+x1);
       dp[u][1] = max(dp[v][1]+x0,x1+dp[v][0]);
    }
    dp[u][1] = max(dp[u][1],dp[u][0]+a[u]); // 这里主要是为了判叶子节点

}
signed main(){
    ios::sync_with_stdio(false);
    cin>>n;
    int rt = 0;
    for(int i=1;i<=n;i++){
        int x;
        cin>>x>>a[i];
        if(x==-1) rt = i;
        else{
            add_edge(i,x);
            add_edge(x,i);
        }
    }
    dfs(rt,0);
    cout<<max(dp[rt][1],dp[rt][0])<<endl;
}

CF296B Yaroslav and Two Strings(计数)

题目链接

https://codeforces.com/problemset/problem/296/B

题意

如果两个只包含数字且长度为 n 的字符串 sw 存在两个数字 1 ≤ i , j ≤ n 1\leq i,j\leq n 1i,jn,使得 s i < w i , s j > w j s_i<w_i,s_j>w_j si<wi,sj>wj,则称 s s s w w w 是不可比的。现在给定两个包含数字和问号且长度为 n 的字符串,问有多少种方案使得将所有问号替换成0到9的数字后两个字符串是不可比的?

input

2
90
09

output

1

input

2
11
55

output

0
思路

考虑 d p [ N ] [ 4 ] dp[N][4] dp[N][4]:

0 : 前面不出现 s j > w j s_j>w_j sj>wj 的情况

1 : 前面不出现 s j < w j s_j<w_j sj<wj 的情况

2:前面既有 s j > w j s_j>w_j sj>wj ,也有 s j < w j s_j<w_j sj<wj 的情况

3:前面全是 s j = w j s_j = w_j sj=wj 的情况

那么,对于当前第 i 项来说,无论是否存在 ′ ? ′ '?' , 都可以分为3种情况:

s i > w i s_i > w_i si>wi

s i = w i s_i = w_i si=wi

s i < w i s_i < w_i si<wi

此时每个 d p [ i ] [ ] dp[i][ ] dp[i][]可以从 d p [ i − 1 ] [ ] dp[i-1][ ] dp[i1][]中继承过来,最后答案即为 d p [ n ] [ 2 ] dp[n][2] dp[n][2]

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+100;
const int mod = 1e9+7;
int dp[N][4];
char s[N],w[N];
signed main(){
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    cin>>(s+1);
    cin>>(w+1);
    dp[0][3] = 1;
    for(int i=1;i<=n;i++){
        if(s[i]!='?'&&w[i]!='?'){
            if(s[i]>w[i]){
                dp[i][0] = 0;
                dp[i][1] = (dp[i-1][1]+dp[i-1][3])%mod;
                dp[i][2] = (dp[i-1][0]+dp[i-1][2])%mod;
                dp[i][3] = 0;
            }
            else if(s[i]<w[i]){
                dp[i][0] = (dp[i-1][0]+dp[i-1][3])%mod;
                dp[i][1] = 0;
                dp[i][2] = (dp[i-1][2]+dp[i-1][1])%mod;
                dp[i][3] = 0;
            }
            else{
                dp[i][0] = dp[i-1][0];
                dp[i][1] = dp[i-1][1];
                dp[i][2] = dp[i-1][2];
                dp[i][3] = dp[i-1][3];
            }
        }
        else if (s[i]=='?'&&w[i]!='?'){
            int big = '9'-w[i];     //  big: s>w的情况数, small: s<w的情况数, equal: s=w情况数 
            int small = w[i]-'0';
            int equal = 1;
            dp[i][0] = (small*dp[i-1][3]%mod+(small+equal)*dp[i-1][0]%mod)%mod;
            dp[i][1] = (big*dp[i-1][3]%mod+(big+equal)*dp[i-1][1]%mod)%mod;
            dp[i][2] = (10*dp[i-1][2]%mod + small*dp[i-1][1]%mod + big*dp[i-1][0]%mod)%mod;
            dp[i][3] = equal*dp[i-1][3]%mod;
        }
        else if(s[i]!='?'&&w[i]=='?'){
            int big = s[i]-'0';	   //  big: s>w的情况数, small: s<w的情况数, equal: s=w情况数 
            int small = '9'-s[i];
            int equal = 1;
            dp[i][0] = (small*dp[i-1][3]%mod+(small+equal)*dp[i-1][0]%mod)%mod;
            dp[i][1] = (big*dp[i-1][3]%mod+(big+equal)*dp[i-1][1]%mod)%mod;
            dp[i][2] = (10*dp[i-1][2]%mod + small*dp[i-1][1]%mod + big*dp[i-1][0]%mod)%mod;
            dp[i][3] = equal*dp[i-1][3]%mod;
        }
        else{
            int big = 45,small = 45,equal = 10;   //显然,当s[i],w[i]都是“?”,一共100种取法,s>w的45种  s=w的10种
            dp[i][0] = (small*dp[i-1][3]%mod+(small+equal)*dp[i-1][0]%mod)%mod;
            dp[i][1] = (big*dp[i-1][3]%mod+(big+equal)*dp[i-1][1]%mod)%mod;
            dp[i][2] = (100*dp[i-1][2]%mod + small*dp[i-1][1]%mod + big*dp[i-1][0]%mod)%mod;
            dp[i][3] = equal*dp[i-1][3]%mod;
        }
    }
    cout<<dp[n][2]%mod<<endl;
}

CF1201D Treasure Hunting(乱搞)

链接

https://codeforces.com/problemset/problem/1201/D

题意

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7WYQT2v-1647866683461)(C:\Users\xi2001\AppData\Roaming\Typora\typora-user-images\image-20220321153025080.png)]

洛谷的中文题面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wCiBfX8g-1647866683461)(C:\Users\xi2001\AppData\Roaming\Typora\typora-user-images\image-20220321153256588.png)]

input

3 3 3 2
1 1
2 1
3 1
2 3

output

6

input

3 5 3 2
1 2
2 3
3 1
1 5

output

8

样例解释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yMvQqiA7-1647866683462)(C:\Users\xi2001\AppData\Roaming\Typora\typora-user-images\image-20220321153428866.png)]

思路

其实思路很简单但是被我写的特别冗杂。

可以把题目理解成一栋楼里有好几条只能向上走的电梯(下面都称作电梯),要求走最少的步数拿到楼里所有的宝藏。

可以发现事实上对于任意一层楼,只有最左的宝藏相邻的两个电梯和最右的宝藏相邻的两个电梯这4个电梯可能被选择。

那么我们只需要一个 d p [ N ] [ 4 ] dp[N][4] dp[N][4] , 让第 i 层和第 i-1 层进行一个4*4的暴力转移就行了。

实现上,我们对每层楼的宝藏位置排个序,找到第一个和最后一个宝藏,然后二分查找相邻的两个电梯。

(其实用lower_bound(),upper_bound()就行了)

这样的复杂度是 O ( 16 ∗ n ∗ l o g n ) O(16*n*logn) O(16nlogn)的。

代码

(怎么我一写代码就写得好乱啊,呜呜呜)

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N= 2e5+1000;
int dp[N][4];
vector<int> v[N];
int n,m,k,q;
int b[N];
const int inf = 1e15;

// cal() 计算在i层楼时,我从from位置开始,到to位置结束,吃完该楼所有宝藏的最短路径(这里写的特别冗杂)
// 事实上就是在一个数轴上从from开始,到to结束,必须经过begin,end这两点的最短路径
int cal(int row,int from,int to){
    int begin = v[row][0],end = v[row][v[row].size()-1]; // 第一个和最后一个宝藏
    if(from>to) swap(from,to);
    if(from==-inf||to==inf) return inf;
    if(begin==end&&from==to) return 2*abs(begin-from);
    if(begin==end){
        if(from<=begin&&begin<=to)
            return abs(from-to);
        else if(begin<=from)
            return to - begin + from - begin;
        else
            return begin-from + begin - to;
    }
    if(from<=begin&&end<=to){
        return to-from;
    }
    if(from>=begin&&to<=end){
        return (to-from)+2*abs(from-begin)+2*abs(end-to);
    }
    if(from>=begin){
        return (to-from)+2*abs(from-begin);
    }
    if(to<=end){
        return (to-from)+2*abs(end-to);
    }
}
// 二分查找第一个位置小于等于now的电梯
int small(int now){
    int l = 0,r = q+1;
    int ans = l;
    while(l<=r){
        int mid = (l+r)>>1;
        if(b[mid]>now){
            r = mid-1;
        }
        else{
            ans = mid;
            l = mid+1;
        }
    }
    return ans;
}
// 二分查找第一个位置大于等于now的电梯
int big(int now){
    int l = 0,r = q+1;
    int ans = r;
    while(l<=r){
        int mid = (l+r)>>1;
        if(b[mid]>now){
            ans = mid;
            r = mid-1;
        }
        else{
            l = mid+1;
        }
    }
    return ans;
}
signed main(){
    ios::sync_with_stdio(false);
    cin>>n>>m>>k>>q;
    v[1].push_back(1);
    for(int i=1;i<=k;i++){
        int x,y;
        cin>>x>>y;
        v[x].push_back(y); // 第x层楼有个宝藏y
    }
    for(int i=0;i<=n;i++){
        for(int j=0;j<4;j++)
            dp[i][j] = inf;
    }
    int mx = n;
    for(int i=n;i>=1;i--){
        if(!v[i].empty()) break;
        mx--;
    }
    for(int i=1;i<=q;i++) cin>>b[i];
    b[0] = -inf;b[q+1] = inf;  // 加两个电梯在正负无穷,避免二分时出锅
    sort(b,b+q+2);   // 给电梯从左到右排个序
    for(int i=1;i<=n;i++){
        if(v[i].empty()) continue;
        sort(v[i].begin(),v[i].end());   // 给每层楼的宝藏排序
    }
    int from[4] = {1,1,1,1};   // from[] 记录上一层楼使用的4个电梯位置
    dp[0][0] = dp[0][1] = dp[0][2] = dp[0][3] = 0;
    for(int i=1;i<=n;i++){
        if(v[i].empty()){
           for(int j=0;j<4;j++) dp[i][j] = dp[i-1][j]; // 如果当前楼层没有宝藏,那显然我们直接坐电梯上去就好
            continue;
        }
        int sa = small(v[i][0]);     // 最左宝藏左边的电梯号
        int sb = big(v[i][0]);        // 最左宝藏右边的电梯号
        int sc = small(v[i][v[i].size()-1]);    // 最右宝藏左边的电梯号
        int sd = big(v[i][v[i].size()-1]);    // 最右宝藏左边的电梯号
        int to[4] = {b[sa],b[sb],b[sc],b[sd]};   // to [] 这层楼使用的4个电梯位置
        
         // 这里要非常注意,到达顶层以后,到最后一个宝藏就可以停下来了,所以顶层的to[] 不再是电梯位置,而是左右两个宝藏的位置
        if(i==mx) to[0] = v[i][0],to[1] = v[i][v[i].size()-1]; 
        for(int j=0;j<4;j++){
            for(int z=0;z<4;z++){
                 // cal() 计算在i层楼时,我从from[z]位置开始,到to[j]位置结束,吃完该楼所有宝藏的最短路径
                int ans = cal(i,from[z],to[j]); 
                dp[i][j] = min(dp[i][j],dp[i-1][z]+ans);
            }
        }
        for(int j=0;j<4;j++) from[j] = to[j];  // 把 from 更新成 to
    }
    int ans = inf;
    for(int i=0;i<4;i++) ans = min(ans,dp[n][i]);
    cout<<ans+(mx-1)<<endl;   // 别忘了向上走也算1步
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值