CF 1107 题解

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;
}
posted @ 2020-10-09 12:52  RainAir  阅读(74)  评论(0编辑  收藏  举报