CF156C Cipher
题意
给出一个只由小写字母组成的字符串
s
s
s ,可以进行若干次操作: 每次选择字符串中连续的两个字符
s
i
,
s
i
+
1
s_i,s_{i+1}
si,si+1 (
i
+
1
≤
∣
s
∣
i+1 \le |s|
i+1≤∣s∣ )。
- 使 s i s_i si 上的字母变成 字母表上 的前一个字母, s i + 1 s_{i+1} si+1 则变成 字母表上 的后一个字母;
- 使 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 1≤t≤104 , ∣ s ∣ ≤ 100 |s| \le 100 ∣s∣≤100
思路
若将
[
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] 集合选数
题意
对于正整数
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 三班不一般
题意
有
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 1≤n≤2⋅105, 1 ≤ a ≤ n + 1 1\le a\le n+1 1≤a≤n+1, 1 ≤ b ≤ 1 0 9 1\le b\le 10^9 1≤b≤109, 1 ≤ l i ≤ r i ≤ 1 0 9 1\le l_i\le r_i\le 10^9 1≤li≤ri≤109
思路
很容易可以列出一个暴力
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=0a−1fi−1,kli>bli≤b
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(ri−max{b,li−1})⋅fi−1,j−1ri≤bri>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=i−a+1i(fj−1⋅第j位的数小于等于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}
fi−1 有关,且
f
i
,
j
f_{i,j}
fi,j 由
f
i
−
1
,
j
−
1
f_{i-1,j-1}
fi−1,j−1 转移来的。若将
f
i
−
1
,
1
−
>
a
−
1
f_{i-1,1->a-1}
fi−1,1−>a−1 看成一个一维数组
A
1
−
>
a
−
1
A_{1->a-1}
A1−>a−1,那么
f
i
,
1
−
>
a
−
1
f_{i,1->a-1}
fi,1−>a−1 就相当于将 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
Aa−1−>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}
Aa−1 分别
×
\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;
}
总结
- 抓住题目中不变的量,利用这一性质设 D P DP DP ;