【hdu 6155】Subsequence Count
题意
给定一个 0101 串 S1...nS1...n 和 QQ 个操作。
操作有 22 种类型:
1. 将 [l,r][l,r] 区间所有数取反(0→1, 1→00→1, 1→0)
2. 询问字符串 SS 的子串 Sl...rSl...r 有多少个不同的子序列,答案模 109+7109+7。
在数学中,某个序列的子序列是从最初序列通过去除某些元素但不破坏余下元素的相对位置(在前或在后)而形成的新序列。
n,q≤105n,q≤105
题解
注意子序列的定义,不是子串!!!
如果是子串的话这题还是挺神仙的,但注意到是子序列后就会发现是个强行二合一(?)的题……
考虑朴素 dpdp,设 dp(i,j)dp(i,j) 表示 S1...iS1...i 以 j(j∈0,1) 结尾的不同子序列数量(下面统一省略“不同”二字)。
设 Si 为 k,则转移为 dp(i,k)=2dp(i−1,k)+dp(i−1,k ^ 1)+1−dp(i−1,k) dp(i,k ^ 1)=dp(i−1,k ^ 1) 解释一下 dp(i,k) 的转移中每一项的含义:
+dp(i−1,k) 表示累加 S1...i−1 中以 k 为结尾的子序列数量;
+dp(i−1,k)+dp(i−1,k ^ 1 表示在 S1...i−1 中以 k 为结尾的子序列后接一个 k 得到的子序列的数量;
+1 是指新增一个长度为 i 的子序列 S1...i。
−dp(i−1,k) 表示第二种情况所统计的子序列 可能与 S1...i−1 中的子序列重复,我们需要去掉重复统计的子序列。由于重复统计的串都是以 Si=k 为结尾,所以重复数量就是 dp(i−1,k)。
dp(i,k ^ 1) 的转移就很好理解了,就是继承 S1...i−1 中以 k ^ 1 结尾的子序列数量,k 不能作为任何子序列的结尾。
显然 dp(i,k)=2dp(i−1,k)+dp(i−1,k ^ 1)+1−dp(i−1,k) 可以简化为 dp(i,k)=dp(i−1,k)+dp(i−1,k ^ 1)+1
然后发现这个 dp 被放到了线段树上,再观察一下 dp 式,大概就知道要把它表示成线性递推形式了。
先把 dp 的第一维去掉,一会我们将优化这一维。
令初始矩阵为一个列数为 3 的行向量,第一项表示 dp(0),第二项表示 dp(1),第三项表示常数 1(就是每次从 dp(i−1,k) 转移到 dp(i,k) 时加的那个 1)。
不难发现,初始矩阵为 [000],若 Si=0 则第 i 位的转移矩阵为 [100110101],若 Si=1 则第 i 位的转移矩阵为 [110010011]。
放到线段树上维护区间内所有转移矩阵的乘积就好了。
至此解决了不带修改的子任务。下面考虑修改,即区间取反对区间内转移矩阵造成的影响。
认(xia)真(ji)分(ba)析(cai)后,发现对于最简单的情况,Si=0 的转移矩阵变成了 Si=1 的转移矩阵,反之亦然。
好像就是把转移矩阵的第 1,2 行交换,再把第 1,2 列交换?(先交换列再交换行的效果一样,证明的话直接考虑每个数的行列变化情况即可)
确实是这样的,直接在线段树上把对应区间的转移矩阵换一下两行两列就行了。
这样做你就漏了严谨性:怎么证明任意多个转移矩阵乘起来(包括两种矩阵混乘),在对区间取反时也是把它交换两行两列呢?
证明:
有一种常见的矩阵,叫初等矩阵。
设一个初等矩阵 E 为单位矩阵 I 交换第 i,j 行,则一个矩阵左乘 E,会交换第 i,j 行。
设一个初等矩阵 E 为单位矩阵 I 交换第 i,j 列,则一个矩阵右乘 E,会交换第 i,j 列。
(其实单位矩阵交换 i,j 两行等于交换 i,j 两列)
对于一个矩阵 F,若 F2=I,则 F=F−1。
所以设本题中初始转移矩阵为 A,区间取反后交换两行两列得到的转移矩阵为 B,则 E×A×E=E×A×E−1=B。
把若干个矩阵左右乘 E 后依次右乘,会发现每对相邻的 E−1 和 E 乘起来得到了 I(E×E−1=E−1×E=1),最后得到了 E×这若干个矩阵依次右乘×E−1,其答案就是“这若干个矩阵依次右乘”得到的矩阵交换两行两列。
Q.E.D
小优化
初始矩阵和转移矩阵可以不对常数 1 开那一维。
令初始矩阵为一个列数为 2 的行向量,第一项表示 dp(0)+1,第二项表示 dp(1)+1。
则初始矩阵为 [11],若 Si=0 则第 i 位的转移矩阵是 [1011],若 Si=1 则第 i 位的转移矩阵是 [1101]。
最后把矩阵的两项之和 −2 就是答案了。
这个优化比较神仙,把常数改了也能用。比如把转移的常数 1 改成 2,把行向量表示的东西分别改成 dp(0)+2,dp(1)+2 即可,最后答案 −4。总之就是要保证所有变量加的常数相同。
拓展
这个性质可以用于一些矩阵快速幂的优化。
考虑对一个矩阵求 An。我们尝试构造一个矩阵 ϕ 和一个能 O(n) 求快速幂的矩阵 B(比如对角线矩阵),使得 ϕ×B×ϕ−1=A。
由于 (ϕ×B×ϕ−1)n=ϕ×B×ϕ−1×ϕ×B×ϕ−1×ϕ×...×ϕ−1=ϕ×Bn×ϕ−1(其实跟本题同理,每对相邻的 ϕ−1 和 ϕ 乘起来得到了 I”),所以我们直接 O(n) 求出 Bn 即可,左右乘 ϕ 就得到了 An。
这样能降低一些矩阵快速幂的复杂度,比如【CF 947E】,这题构造出 ϕ,B 矩阵后,由于矩阵很特殊,用 FFT 做矩乘可以把复杂度做到 O(nlogn)。当然这个做法不是通用的,因为存在一组符合要求的矩阵 ϕ,B 的题目并不多。
而且肉眼并不容易构造出 ϕ,B 的矩阵,详情请咨询 scb 大佬(雾)
#include<bits/stdc++.h>
#define N 100010
#define mod 1000000007
using namespace std;
inline int read(){
int x=0; bool f=1; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=0;
for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
if(f) return x; return 0-x;
}
struct Matrix{
int x[2][2];
Matrix(){x[0][0]=x[1][1]=x[0][1]=x[1][0]=0;}
Matrix(int a, int b){x[0][0]=a, x[0][1]=b, x[1][0]=x[1][1]=0;}
Matrix(int a, int b, int c, int d){x[0][0]=a, x[0][1]=b, x[1][0]=c, x[1][1]=d;}
inline void change(){swap(x[0][0],x[1][1]), swap(x[0][1],x[1][0]);}
inline int* operator [](int p){
return x[p];
}
Matrix operator * (Matrix y)const{
Matrix ret=Matrix();
for(int i=0; i<2; ++i)
for(int k=0; k<2; ++k)
if(x[i][k])
for(int j=0; j<2; ++j)
ret[i][j] = (ret[i][j] + 1ll * x[i][k] * y[k][j] % mod) % mod;
return ret;
}
/*
inline bool operator != (const Matrix &y){
return x[0][0]!=y[0][0] || x[0][1]!=y[0][1] || x[1][0]!=y[1][0] || x[1][1]!=y[1][1];
}*/
}I,chushi,zhuanyi[2];
int n,q;
char s[N];
namespace SegTree{
#define ls o<<1
#define rs o<<1|1
struct Tree{Matrix sum; bool tag;}tr[N<<2];
inline void pushup(int o){
tr[o].sum=tr[ls].sum*tr[rs].sum;
}
void pushdown(int o){
if(tr[o].tag){
tr[ls].sum.change(), tr[rs].sum.change();
tr[ls].tag^=1, tr[rs].tag^=1;
tr[o].tag=0;
}
}
void build(int o, int l, int r){
if(l==r){tr[o].sum=zhuanyi[s[l]-'0']; return;}
int mid=l+r>>1;
build(ls,l,mid), build(rs,mid+1,r);
pushup(o);
}
void mdf(int o, int l, int r, int L, int R){
if(L<=l && r<=R){
tr[o].sum.change();
tr[o].tag^=1;
return;
}
int mid=l+r>>1;
pushdown(o);
if(L<=mid) mdf(ls,l,mid,L,R);
if(R>mid) mdf(rs,mid+1,r,L,R);
pushup(o);
}
Matrix query(int o, int l, int r, int L, int R){
if(L<=l && r<=R) return tr[o].sum;
int mid=l+r>>1; Matrix ret=I;
pushdown(o);
if(L<=mid) ret = ret * query(ls,l,mid,L,R);
if(R>mid) ret = ret * query(rs,mid+1,r,L,R);
return ret;
}
inline int query(int l, int r){
Matrix tmp=chushi*query(1,1,n,l,r);
//cout<<tmp[0][0]<<' '<<tmp[0][1]<<' '<<tmp[1][0]<<' '<<tmp[1][1]<<endl;
//tmp=chushi*tmp;
return (0ll+tmp[0][0]+tmp[0][1]-2+mod)%mod;
}
inline void mdf(int l, int r){
mdf(1,1,n,l,r);
}
#undef ls
#undef rs
}using namespace SegTree;
int main(){
I=Matrix(1,0,0,1), chushi=Matrix(1,1), zhuanyi[0]=Matrix(1,0,1,1), zhuanyi[1]=Matrix(1,1,0,1);
n=read(), q=read();
scanf("%s",s+1);
build(1,1,n);
int type,l,r;
for(int i=1; i<=q; ++i){
type=read(), l=read(), r=read();
if(type==1) mdf(l,r);
else printf("%d\n",query(l,r));
}
return 0;
}
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(四):结合BotSharp
· 软件产品开发中常见的10个问题及处理方法
· Vite CVE-2025-30208 安全漏洞
· MQ 如何保证数据一致性?
· 《HelloGitHub》第 108 期