一些热门考点的练习记录

CF156C Cipher

link

题意
给出一个只由小写字母组成的字符串 s s s ,可以进行若干次操作: 每次选择字符串中连续的两个字符 s i , s i + 1 s_i,s_{i+1} si,si+1 i + 1 ≤ ∣ s ∣ i+1 \le |s| i+1s )。

  1. 使 s i s_i si 上的字母变成 字母表上 的前一个字母, s i + 1 s_{i+1} si+1 则变成 字母表上 的后一个字母;
  2. 使 s i s_i si 上的字母变成 字母表上 的后一个字母, s p + 1 s_{p+1} sp+1 则变成 字母表上 的前一个字母;

注意: a a a 不能变成前一个字母, z z z 也不能变成后一个字母。求可以生成多少种与原字符串不同的字符串,答案对 1 0 9 + 7 10^9+7 109+7 取模。多测,每组有 t t t 个字符串。 1 ≤ t ≤ 1 0 4 1 \le t \le 10^4 1t104 , ∣ s ∣ ≤ 100 |s| \le 100 s100

思路
若将 [ a , z ] [a,z] [a,z] 转为 [ 1 , 26 ] [1,26] [1,26] ,发现无论怎么变化 和不变 ,想到任意相同长度的字符串只要保证和不变,都能由原字符串得到。那就可以将题目转化成:求长度为 ∣ s ∣ |s| s 和为 s u m sum sum 的字符串个数。用 D P DP DP 即可解决。

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=105,mod=1e9+7;
int f[maxn][maxn*26];
string st;
int main(){
	f[0][0]=1;
	for(int i=1;i<=100;i++)
		for(int j=1;j<=2600;j++)
			for(int k=1;k<=26;k++)
				if(j-k>=0) (f[i][j]+=f[i-1][j-k])%=mod;
	int t; cin>>t;
	while(t--){
		cin>>st; int len=st.size(),sum=0;
		for(int i=0;i<len;i++)
			sum+=(st[i]-'a'+1);
		cout<<f[len][sum]-1<<"\n";
	}
	return 0;
}

[HNOI2012] 集合选数

link

题意
对于正整数 n n n,需要求出集合 { 1 , 2 , … , n } \{1,2,…,n\} {1,2,,n} 的子集满足集合中的数两两之商不为 2 2 2 3 3 3 的子集个数(答案需对 1 0 9 + 1 10^9+1 109+1 取模)。

思路
小型 构造题
第一行第一列的元素为 p p p p p p 不是 2 2 2 也不是 3 3 3 的倍数),每一行的后面所有数均为前面的数的两倍,每列的数是它上面的数的三倍,即如下:

p    2p   4p   8p   16p ...
3p   6p   12p  24p ...
9p   18p  36p ...
27p  54p ...

发现在上面这个矩阵选数,选出的数相互在矩阵中不相邻(只限上下左右四个方向),那么这就是一个合法集合,所以题目就变成了求独立集个数。
那就设 f i , j f_{i,j} fi,j 表示矩阵中操作完前 i i i 行且第 i i i 行选的数的集合为 j j j 的方案数。
枚举 p p p ,对于每个 p p p 构成的矩阵做一次 d p dp dp ,将每次的答案乘起来即可。时间复杂度为 O ( 能过 ) O(能过) O(能过)

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll mod=1e9+1;

int n;
vector<int>vec[20];
ll Solve(int x){
	for(int j=1;j<=17;j++) 
		vec[j].clear();
	int cnt=0;
	while(x<=n){
		int y=x; cnt++;
		while(y<=n){
			vec[cnt].push_back(y);
			y*=3;
		}
		x*=2;
	}
	int f[cnt+2][(1<<vec[1].size())+2],s=(1<<vec[1].size())-1;
	for(int i=0;i<=cnt;i++)
		for(int j=0;j<=s;j++) 
			f[i][j]=0;
	ll sum=0;
	f[0][0]=1;
	for(int i=1;i<=cnt;i++){
		int s2=(1<<vec[i].size())-1;
		for(int j=0;j<=s2;j++){
			bool ok=1;
			for(int k=0;k<vec[i].size()-1;k++)//不能在同一行中同时选相邻的两个数 
				if((j&(1<<k))&&(j&(1<<(k+1)))){ ok=0; break; }
			if(ok){
				(f[i][j]+=f[i-1][0])%=mod;
				for(int k=(j^s);k;k=((k-1)&(j^s)))
					(f[i][j]+=f[i-1][k])%=mod;
				if(i==cnt) (sum+=f[i][j])%=mod;
			}
		}
	}
	return sum;
} 

int main(){
//	freopen("clone.in","r",stdin);
//	freopen("clone.out","w",stdout);
	cin>>n;
	ll ans=1;
	for(int i=1;i<=n;i++)
		if(i%2!=0&&i%3!=0) (ans*=Solve(i))%=mod;
	cout<<ans;
	return 0;
}

「DBOI」Round 1 三班不一般

link

题意
n n n 个位置,每个位置 i i i 可以在区间 [ l i , r i ] [l_i,r_i] [li,ri] 中选择一个数。当有连续 a a a 个位置的数都大于 b b b 时,这种方案便不合法。求有多少种合法方案,答案对 998244353 998244353 998244353 取模。

1 ≤ n ≤ 2 ⋅ 1 0 5 1\le n\le 2\cdot 10^5 1n2105 1 ≤ a ≤ n + 1 1\le a\le n+1 1an+1 1 ≤ b ≤ 1 0 9 1\le b\le 10^9 1b109 1 ≤ l i ≤ r i ≤ 1 0 9 1\le l_i\le r_i\le 10^9 1liri109

思路
很容易可以列出一个暴力 D P DP DP 转移。 40 p t s 40pts 40pts
f i , j f_{i,j} fi,j 表示位置 i i i 是连续的第 j j j 个位置上的数大于 b b b 的位置时的方案数。
f i , 0 = { 0 l i > b ( min ⁡ { b , r i } − l i + 1 ) ⋅ ∑ k = 0 a − 1 f i − 1 , k l i ≤ b f_{i,0} = \begin{cases} 0 & l_i \gt b \\ (\min\{b,r_i\}-l_i+1) \cdot \sum_{k=0}^{a-1} f_{i-1,k} & l_i \le b \end{cases} fi,0={0(min{b,ri}li+1)k=0a1fi1,kli>blib

f i , j = { 0 r i ≤ b ( r i − max ⁡ { b , l i − 1 } ) ⋅ f i − 1 , j − 1 r i > b f_{i,j} = \begin{cases} 0 & r_i \le b \\ (r_i-\max\{b,l_i-1\}) \cdot f_{i-1,j-1} & r_i \gt b \end{cases} fi,j={0(rimax{b,li1})fi1,j1ribri>b
但是这样的空间复杂度为 O ( a n ) O(an) O(an) ,时间复杂度为 O ( a n ) O(an) O(an)

优化一: 60 p t s 60pts 60pts
考虑将二维压成一维,重新设状态 f i f_i fi 表示到第 i i i 个位置时合法的方案数。
枚举 j ,表示从 f j f_j fj 转移过来。
转移大概是 f i = ∑ j = i − a + 1 i ( f j − 1 ⋅ 第 j 位的数小于等于 b 的方案数 ⋅ 第 j + 1 位到第 i 位上的数都大于 b 的方案数 ) f_i = \sum_{j=i-a+1}^{i} (f_{j-1} \cdot 第j位的数小于等于b的方案数 \cdot 第j+1位 到 第i位上的数都大于b的方案数) fi=j=ia+1i(fj1j位的数小于等于b的方案数j+1位到第i位上的数都大于b的方案数)
这样空间就将为 O ( n ) O(n) O(n) ,时间依然为 O ( a n ) O(an) O(an)
优化二 100 p t s 100pts 100pts
发现 f i f_i fi 只和 f i − 1 f_{i-1} fi1 有关,且 f i , j f_{i,j} fi,j f i − 1 , j − 1 f_{i-1,j-1} fi1,j1 转移来的。若将 f i − 1 , 1 − > a − 1 f_{i-1,1->a-1} fi1,1>a1 看成一个一维数组 A 1 − > a − 1 A_{1->a-1} A1>a1,那么 f i , 1 − > a − 1 f_{i,1->a-1} fi,1>a1 就相当于将 A 数组中的值全部往后推一位,即 A 0 − > A 1 A_0 -> A_1 A0>A1 A 1 − > A 2 A_1 -> A_2 A1>A2, …… , A a − 1 − > A a A_{a-1} -> A_a Aa1>Aa 。然后 A 0 = ∑ i = 1 a A i A_0 = \sum_{i=1}^{a} A_i A0=i=1aAi ,最后再将 A 1 A_1 A1 A 2 A_2 A2 ,…… , A a − 1 A_{a-1} Aa1 分别 × \times × i i i 个位置能填的数大于 b b b 的方案数。 A a A_a Aa 就没用了。
发现这个其实可以通过线段树去维护,我们可以不用对数组整体往后移一位,就是说每次直接在那段有用区间的前一位插一个数,将有用区间往前移一位即可,最多只会插入 n + 1 n+1 n+1 个数(因为还有 f 0 f_0 f0)(就是说最开始有用区间为 [ n + 1 , n + 1 ] [n+1,n+1] [n+1,n+1])。
时间复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn)

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5,mod=998244353;

struct TREE{ ll sum,tag; }tree[maxn*4];
void Build(int rt,int l,int r){
	tree[rt]={0,1};
	if(l==r) return;
	int mid=(l+r)>>1;
	Build(rt*2,l,mid);
	Build(rt*2+1,mid+1,r);
}

void Pushdown(int rt,int l,int r){
	if(tree[rt].tag==1) return;
	int mid=(l+r)>>1,ls=rt*2,rs=ls+1;
	(tree[ls].sum*=tree[rt].tag)%=mod;
	(tree[ls].tag*=tree[rt].tag)%=mod;
	(tree[rs].sum*=tree[rt].tag)%=mod;
	(tree[rs].tag*=tree[rt].tag)%=mod;
	tree[rt].tag=1;
}

void Modify_new(int rt,int l,int r,int x,ll y){
	if(l==r){
		tree[rt].sum=y;
		return;
	}
	Pushdown(rt,l,r);
	int mid=(l+r)>>1;
	if(x<=mid) Modify_new(rt*2,l,mid,x,y);
	else Modify_new(rt*2+1,mid+1,r,x,y);
	tree[rt].sum=(tree[rt*2].sum+tree[rt*2+1].sum)%mod;
}

void Modify(int rt,int l,int r,int x,int y,ll z){
	if(x<=l&&r<=y){
		(tree[rt].sum*=z)%=mod;
		(tree[rt].tag*=z)%=mod;
		return;
	}
	Pushdown(rt,l,r);
	int mid=(l+r)>>1;
	if(x<=mid) Modify(rt*2,l,mid,x,y,z);
	if(y>mid) Modify(rt*2+1,mid+1,r,x,y,z);
	tree[rt].sum=(tree[rt*2].sum+tree[rt*2+1].sum)%mod;
}

ll Query(int rt,int l,int r,int x,int y){
	if(x<=l&&r<=y) return tree[rt].sum;
	Pushdown(rt,l,r);
	int mid=(l+r)>>1; ll s=0;
	if(x<=mid) (s+=Query(rt*2,l,mid,x,y))%=mod;
	if(y>mid) (s+=Query(rt*2+1,mid+1,r,x,y))%=mod;
	return s;
}

struct EDGE{ ll x,y; }a[maxn];
int main(){
	int n,ss; ll ld; cin>>n>>ss>>ld;
	for(int i=1;i<=n;i++)
		cin>>a[i].x>>a[i].y;
	Build(1,1,n+1);
	int hd=n+1,tal=n+1;
	Modify_new(1,1,n+1,hd,1);// f[0][0]=1
	for(int i=1;i<=n;i++){
		ll sum=Query(1,1,n+1,hd,tal)*max(0ll,min(ld,a[i].y)-a[i].x+1)%mod;// f[i-1][0] + f[i-1][1] + ... + f[i-1][ss-1]
		hd--,tal=min(hd+ss-1,n+1);
		Modify_new(1,1,n+1,hd,sum);// f[i][0] = f[i-1][0] + f[i-1][1] + ... + f[i-1][ss-1]
		Modify(1,1,n+1,hd+1,tal,max(0ll,a[i].y-max(ld,a[i].x-1)));// f[i][1 ~ ss-1] = f[i-1][1 ~ ss-1] * 第 i 个宿舍能开的灯的亮度大于 ld 的数量 
	}
	cout<<Query(1,1,n+1,hd,tal);// f[n][0] + f[n][1] + ... + f[n][ss-1] 
	return 0;
}

总结

  1. 抓住题目中不变的量,利用这一性质设 D P DP DP
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值