A

如果长度为 $2$ 特判,否则将第一个数和剩下的数分开。

B

$B$ 进制下正整数的数根等价于 $\pmod {B-1}$,注意这里 $0$ 对应的答案是 $B-1$。

所以答案是 $(k-1)B+x$。

C

双指针扫出所有相同的段,每段取前 $k$ 大即可。

D

观察一下发现只需要枚举 $x|n$,然后前缀和预处理+判断所有矩阵是不是全 $0/1$ 矩阵即可。

时间复杂度分析一波:先考虑行的贡献,枚举的每一个 $x|n$ 都会贡献 $\frac{n}{x}$ ,那么相当于是 $\sigma_1(n)$,所以复杂度是 $O(\sigma_1(n)^2)$。这是可以分析出复杂度 $O(n^2)$ 的:考虑将 $n$ 的所有因子分类,对于 $>\sqrt{n}$ 的,只有 $O(1)$ 个,所以复杂度贡献 $O(n)$;对于 $<\sqrt{n}$ 的,因为 $\sqrt{n} \times \sqrt{n} = n$,所以他们的和顶多贡献总复杂度 $O(n)$,所以 $\sigma_1(n)$ 的量级可以认为是 $O(n)$ 的。

E

这种区间 dp 模型都没见过。。再看了两道

CF 1132 F

题目链接

每次删除相同的连续段有一个性质:互不包含的删除可以任意调换相对顺序。所以我们对于一个区间可以钦定左端点是最后被删除的。

这个题就可以设 $f_{l,r}$ 表示删除区间 $[l,r]$ 的代价,转移首先可以花 $1$ 代价删除 $l$,或者是枚举一个 $x$,满足 $s_l = s_x$,将 $l$ 并入 $x$,代价就是 $f_{l+1,x-1}+f_{x,r}$。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 500+5;
int f[MAXN][MAXN],n;
char str[MAXN];
// 删除操作 一定互相包含 不交的连通块顺序无关
// 删除首先枚举 mid,那么如果str[l]==str[mid]先删除[l+1,mid-1]然后l和mid就并起来了

int main(){
    scanf("%d",&n);
    scanf("%s",str+1);
    CLR(f,0x3f);
    FOR(i,1,n) f[i][i] = 1,f[i][i-1] = 0;
    FOR(len,2,n){
        FOR(l,1,n-len+1){
            int r = l+len-1;
            f[l][r] = std::min(f[l+1][r],f[l][r-1])+1;
            FOR(k,l+1,r){
                if(str[k] == str[l]){
                    f[l][r] = std::min(f[l][r],f[l+1][k-1]+f[k][r]);
                }
            }
        }
    }
    printf("%d\n",f[1][n]);
    return 0;
}

SPOJ ZUMA

题目链接

这个题我们需要知道连续段的长度了,还是用左端点最后删除的思路,设 $f_{l,r,k}$ 表示区间 $[l,r]$,$l$ 前面删除导致接了 $k$ 个和 $s_l$ 相同的数的答案。转移的时候首先可以加一个数在左端点左边,$f_{l,r,k} = f_{l,r,k+1}+1$,然后如果现在可以消除了(也就是 $k-1 == K$),那就可以消除,也就是 $f_{l,r,K} = f_{l+1,r,0}$。然后就是枚举和位置 $x$ 拼在一起,$f_{l,r,k} = f_{l+1,x-1,0}+f_{x,r,\min\{k+1,K-1\}}$。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 100+5;
int n,K,a[MAXN];
int f[MAXN][MAXN][6];

inline void upmin(int &x,int y){
    if(x > y) x = y;
}

int main(){
    scanf("%d%d",&n,&K);
    FOR(i,1,n) scanf("%d",a+i);
    CLR(f,0x3f);
    FOR(i,1,n) FOR(j,0,K-1) f[i][i][j] = K-j-1;
    FOR(i,1,n) f[i][i-1][0] = 0;
    FOR(len,2,n){
        FOR(l,1,n-len+1){
            int r = l+len-1;
            ROF(k,K-1,0){
                upmin(f[l][r][k],f[l][r][std::min(K-1,k+1)]+1);
                if(k == K-1) upmin(f[l][r][k],f[l+1][r][0]);
                FOR(x,l+1,r){
                    if(a[l] == a[x]) upmin(f[l][r][k],f[l+1][x-1][0]+f[x][r][std::min(K-1,k+1)]);
                }
            }
        }
    }
    printf("%d\n",f[1][n][0]);
    return 0;
}
/*
每次消除连续段的 如果要保留连续段消息 就要设f[i][j][k] 表示区间[i,j],i前面接了长度为k的一段和i相同的还未删除的数的方案数 然后枚举消去
转移 首先可以加入一个: f[i][j][k] = f[i][j][k+1]+1
然后可以消去k个: f[i][j][k] = f[i+1][j][0]
然后可以和后面的合并(如果相等): f[i][j][k] = f[i+1][j][k+1]
*/

CF 1107 E

首先我们先求出 $g_i$ 表示将长度为 $i$ 的区间消光的答案,这个可以用背包解决。

然后,我们这个题也是要知道长度才能计算贡献,所以设 $f_{l,r,k}$ 表示 $[l,r]$ 前面跟着长度 $k$ 的答案,转移的时候可以先删掉这一段,$f_{l,r,k} = f_{l+1,r,0}+g_{k+1}$,也可以枚举下一个位置,和上一个题一样转移。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 100+5;

int n;
char str[MAXN];
LL a[MAXN];int b[MAXN];
LL f[MAXN][MAXN][MAXN];// 区间[l,r],l接了长度为k的一段

inline void upmax(LL &x,LL y){
    if(x < y) x = y;
}

int main(){
    scanf("%d",&n);
    scanf("%s",str+1);
    FOR(i,1,n) scanf("%lld",b+i);
    FOR(i,1,n){
        FOR(j,0,i-1){
            upmax(a[i],a[j]+b[i-j]);
        }
    }
    CLR(f,~0x3f);
    FOR(i,1,n){
        FOR(j,0,n-1) f[i][i][j] = a[j+1];
        f[i][i-1][0] = 0;
    }
    FOR(len,2,n){
        FOR(l,1,n-len+1){
            int r = l+len-1;
            FOR(k,0,n){
                upmax(f[l][r][k],f[l+1][r][0]+a[k+1]);
                FOR(x,l+1,r){
                    if(str[x] == str[l]) upmax(f[l][r][k],f[l+1][x-1][0]+f[x][r][k+1]);
                }
            }
        }
    }
    printf("%lld\n",f[1][n][0]);
    return 0;
}
/*
区间连续段+删除相关dp可以记左端点前面的个数
转移 枚举一个str[l]==str[x]
f[l][r][k] = f[l+1][x-1][0]+f[x][r][k+1]
f[l][r][k] = f[l][r][0]+a[k+1]
*/

总结:区间 dp 如果有时候可以不注意操作顺序,就可以定义最有利于转移的顺序(这种模型中就是最后删除左端点),然后记录与其相关的值(最左边延长了多少)。这种删除模型的核心是删掉了后会接起来,所以记录端点往左延伸多少也没错。

另一个区间dp:每次可以选择一段区间染色,求最少到目标状态的步数。

先考虑染色操作有时候不能分成两个区间的原因是可能会跨段染色,于是我们可以

设 $f_{l,r}$ 表示 $[l,r]$ 的答案,每次转移的时候如果 $c_l = c_r$ 那么我们可以用 $f_{l+1,r}$ 染完之后顺便把 $l$ 染了,也就是 $f_{l,r}$ 可以用 $f_{l+1,r},f_{l,r-1}$ 更新。

如果不相等,说明这两个点属于两次不同的染色,然后就可以枚举断点更新了。

F

直接正着做不太好处理每个位置前面对自己的贡献(状态会爆炸),我们倒着做,反而考虑每个位置对别人的贡献。首先贷款肯定是紧密连接的(要不然不优)。设 $f_{i,j}$ 表示考虑到第 $i$ 个贷款,当前选了 $j$ 个的方案数,转移首先可以不选这个点,$f_{i,j} \to f_{i+1,j}$,或者可以选这个点,$f_{i,j} \to f_{i+1,j+1}+a_i-b_i\times\min(k_i,j)$。

但是发现这样 dp 的顺序不对,按照我们的方法我们应该贪心地让 $b_i$ 大的先被考虑。

但是发现还是不对(这样写你应该会发现第二个样例答案少了 $1$),发现原因是有些小的数已经覆盖了超过最右边的点,所以可以将这个点往前拿减少这个点的贡献,并且后面的点由于没有覆盖超,贡献是不变的。这种情况我们直接把后面的点全都一直往后拿就行,具体要加个转移 $f_{i,j} \to f_{i+1,j} + a_i-b_ik_i$。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 500+5;

struct Node{
    LL a,b,k;
    
    inline bool operator < (const Node &t) const {
        return b > t.b;
    }
}v[MAXN];
int n;
LL f[MAXN][MAXN];
// i 个,选了j个

int main(){
    scanf("%d",&n);
    FOR(i,1,n) scanf("%lld%lld%lld",&v[i].a,&v[i].b,&v[i].k);
    std::sort(v+1,v+n+1);
    CLR(f,-0x3f);
    f[0][0] = 0;
    FOR(i,1,n){
        FOR(j,0,i){
            f[i][j] = f[i-1][j];
            f[i][j] = std::max(f[i][j],f[i-1][j]+v[i].a-v[i].k*v[i].b);
            if(j-1 >= 0) f[i][j] = std::max(f[i][j],f[i-1][j-1]+v[i].a-std::min((LL)(j-1),v[i].k)*v[i].b);
        }
        // DEBUG(f[i][0]);
    }
    LL ans = -1e18;
    FOR(i,0,n) ans = std::max(ans,f[n][i]);
    printf("%lld\n",ans);
    return 0;
}

G

感觉难度倒序啊。。。

这个题首先枚举右端点,然后考虑计算所有左端点的答案,发现那个 $\max$ 固定 $r$ 后是个后缀和的形式,所以用单调栈维护一下所有后缀最大值,算一下贡献插入 multiset ,没了。

如果想做到 $O(n)$ 的话,首先 ST 表那一部分可以用每次暴力扫,删除栈顶的时候直接获得栈顶的最小值贡献来优化掉。

multiset 那一部分其实可以对栈记录一个前缀最小值,这样每次更新就好了。(主要还是为了 vp 的时候避免细节快点写用了 $\log$)

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 3e5+ 5;
const int MAXM = 18;

struct Node{
    LL val,ps,gx;
    Node(LL val=0,LL ps=0,LL gx=0) : val(val),ps(ps),gx(gx) {}
};

int n,a;
int d[MAXN];
LL c[MAXN];
Node st[MAXN];
int tp;
LL mn[MAXM+1][MAXN];// 求最小值
int pc[MAXN];

inline LL calc(int l,int r){
    int c = pc[r-l+1];
    l = std::max(l,0);r = std::min(r,n);
    return std::min(mn[c][l],mn[c][r-(1<<c)+1]);
}

std::multiset<LL> S;

int main(){
    pc[0] = -1;FOR(i,1,MAXN-1) pc[i] = pc[i>>1]+1;
    scanf("%d%d",&n,&a);
    FOR(i,1,n) scanf("%d%lld",d+i,c+i);
    FOR(i,1,n) c[i] = a-c[i],c[i] += c[i-1];
    FOR(i,1,n) mn[0][i] = c[i];
    FOR(i,1,MAXM){
        for(int j = 0;j+(1<<(i-1)) < MAXN;++j){
            mn[i][j] = std::min(mn[i-1][j],mn[i-1][j+(1<<(i-1))]);
        }
    }
    LL ans = 0;
    FOR(r,1,n){
        ans = std::max(ans,c[r]-c[r-1]);
        if(r != 1){
            LL now = 1ll*(d[r]-d[r-1])*(d[r]-d[r-1]);
            while(tp && st[tp].val <= now){
                S.erase(S.find(st[tp].gx));
                tp--;
            }
            st[tp+1] = Node(now,r,calc(st[tp].ps-1,r-1)+now);
            ++tp;S.insert(st[tp].gx);
        }
        if(!S.empty()) ans = std::max(ans,c[r]-*S.begin());
    }
    printf("%lld\n",ans);
    return 0;
}
Last modification:October 9th, 2020 at 12:53 pm
如果觉得我的文章对你有用,请随意赞赏