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;
}