Codeforces 题目传送门 & 洛谷题目传送门

Yet another 自己搞出来的难度 \(\ge 2800\) 的题

介绍一个奇奇怪怪的 \(n\log n\) 的做法。首先特判掉字符串中全是 \(0\) 的情况,这种情况答案显然为 \(n\)。我们假设字符串中 \(1\) 的位置为 \(p_1,p_2,\cdots,p_k\)。考虑当我们已经求出了 \(s[1...p_i]\) 可以得到多少个不同的 01 串后,怎样求出 \(s[1...p_{i+1}]\) 可以得到多少个不同的 01 串。

  • 显然原来 \(s[1...p_i]\) 可以得到的子串,也可以通过 \(s[1...p_{i+1}]\) 得到,因为我们可以将 \(p_{i+1}\) 与 \(p_{i+1}-1\) 合并,\(p_{i+1}-1\) 与 \(p_{i+1}-2\) 合并,……,\(p_i+1\) 与 \(p_i\) 合并,这样我们就合并得到了 \(s[1...p_i]\)。对于 \(s[1...p_i]\) 可以得到的子串 \(t\),再通过 \(s[1...p_i]\to t\) 的合并方式即可合并得到 \(t\)。
  • 对于原来 \(s[1...p_i]\) 可以得到的子串 \(t\),在其后面添上 \(j(0\le j\le p_{i+1}-p_i-1)\) 个 \(0\) 和一个 \(1\) 得到的字符串也可以被合并出来,因为我们可以按照 \(s[1...p_i]\to t\) 的合并方式合并 \(s[1...p_{i+1}]\) 的前 \(p_i\) 位,然后把最后一段 \(00...01\) 合并只剩 \(j\) 个 \(0\) 和 \(1\) 个 \(1\)。

假设 \(dp_i\) 为 \([1...p_i]\) 可以得到多少个 01 串,照这样讲,难道就有 \(dp_{i+1}=(p_{i+1}-p_i)\times dp_i\) 吗?

非也,不然您觉得这题咋就能放到 D1E 呢/ww

这样做的问题是,会出现重复计算的字符串,比方说 \(s=\texttt{10101}\),那么当我们计算 \(s[1...p_i]\) 可以得到的子串(第一类)时会统计到字符串 \(\texttt{101}\),而 \(s[1...p_i]\) 还可以得到一个字符串 \(\texttt{1}\),在 \(\texttt{1}\) 后面添上一个 \(0\) 和一个 \(1\) 后也能得到 \(\texttt{101}\),也就是说 \(\texttt{101}\) 被统计了两次。

怎样避免这种情况呢?显然这种算重的情况只会发生在第一类与第二类之间,因为每一类内部的字符串是不会算重的(对于上述转移中的第一种情况,原来 \(s[1...p_i]\) 根据我们 \(dp\) 状态的定义已经“本质不同”了,不会重复计算,而对于上述转移中的第二种情况,要么最后一段 \(0\) 的个数 \(j\) 不同,要么去掉最后一段 \(00...01\) 之后,得到的可以通过 \(s[1...p_i]\) 合并得到的子串不同,也不会算重),也就是说算重的字符串只可能是对于某个 \(s[1...p_i]\) 得到的 \(t\),它加上 \(j\) 个 \(0\) 和一个 \(1\) 后得到的字符串照样可以通过 \(s[1...p_i]\) 得到。因此我们可以转化为统计有多少个可以被 \(s[1...p_i]\) 的字符串,它最后一段 \(00...01\) 中 \(0\) 的个数在 \([0,p_{i+1}-p_i-1]\) 中。

考虑换个状态设计,\(dp_{i,j}\) 表示考虑 \([1...p_i]\) 的字符串,有多少个满足最后一段 \(00...01\) 中 \(0\) 的个数为 \(j\),这样一来转移就比较明显了,记 \(S=\sum\limits_{j}dp_{i,j}\),对于 \(j>p_{i+1}-p_i-1\),显然用第二种情况转移得来的字符串不可能得到最后一段 \(j\) 个 \(0\) 的 01 串,故 \(dp_{i+1,j}=dp_{i,j}\),对于 \(j\le p_{i+1}-p_i-1\),第一种情况贡献 \(dp_{i,j}\) 个字符串,第二种情况贡献 \(S\) 个字符串,但根据上面的推论还有 \(dp_{i,j}\) 个字符串被算重,故 \(dp_{i+1,j}=S+dp_{i,j}-dp_{i,j}=S\),还有一个小问题,就是对于形如 \(000...01\) 的字符串,它不能通过某个可以通过 \(s[1...p_i]\) 得到的字符串加上若干个 \(0\) 和一个 \(1\) 得到(因为它去掉最后一段 \(00...01\) 后就得到空串了,而空串不可能被我们得到),而显然如果 \(\begin{matrix}\underbrace{000...00}1\\j\text{个}0\end{matrix}\) 能够被 \(s[1...p_i]\) 得到,那么 \(j\le p_1-1\)。也就是说对于 \(j\le p_1-1\),字符串 \(\begin{matrix}\underbrace{000...00}1\\j\text{个}0\end{matrix}\) 被包含在了 \(dp_{i,j}\) 中,但事实上它没有被重复统计,故被重复统计的只有 \(dp_{i,j}-1\) 个,即 \(dp_{i+1,j}=S+dp_{i,j}-(dp_{i,j}-1)=S+1\),于是我们有状态转移方程:

\[dp_{i+1,j}=\begin{cases}S+1&j\le\min(p_1-1,p_{i+1}-p_i-1)\\S&p_1\le j\le p_{i+1}-p_i-1\\dp_{i,j}&j>p_{i+1}-p_i-1\end{cases}
\]

这样暴力复杂度是平方的,但借鉴 CF115E 的套路第一维显然可以去掉,用线段树维护第二维,只需实现一个区间赋值和全局求和,复杂度 \(n\log n\)。

又一次抢了最劣解(bushi

const int MOD=1e9+7;
const int MAXN=1e6;
char str[MAXN+5];int n;
struct node{int l,r,sum,lz;} s[MAXN*4+5];
void pushup(int k){s[k].sum=(s[k<<1].sum+s[k<<1|1].sum)%MOD;}
void build(int k,int l,int r){
s[k].l=l;s[k].r=r;s[k].lz=-1;if(l==r) return;
int mid=l+r>>1;build(k<<1,l,mid);build(k<<1|1,mid+1,r);
}
void pushdown(int k){
if(~s[k].lz){
s[k<<1].sum=1ll*(s[k<<1].r-s[k<<1].l+1)*s[k].lz%MOD;
s[k<<1|1].sum=1ll*(s[k<<1|1].r-s[k<<1|1].l+1)*s[k].lz%MOD;
s[k<<1].lz=s[k].lz;s[k<<1|1].lz=s[k].lz;s[k].lz=-1;
}
}
void modify(int k,int l,int r,int x){
if(l>r) return;
if(l<=s[k].l&&s[k].r<=r){
s[k].sum=1ll*(s[k].r-s[k].l+1)*x%MOD;
s[k].lz=x;return;
} pushdown(k);int mid=s[k].l+s[k].r>>1;
if(r<=mid) modify(k<<1,l,r,x);
else if(l>mid) modify(k<<1|1,l,r,x);
else modify(k<<1,l,mid,x),modify(k<<1|1,mid+1,r,x);
pushup(k);
}
int main(){
scanf("%s",str+1);n=strlen(str+1);
int fst=0;for(int i=1;i<=n;i++) if(str[i]^48){fst=i;break;}
if(!fst) return printf("%d\n",n),0;int pre=fst;
build(1,0,n);modify(1,0,fst-1,1);
for(int i=fst+1;i<=n;i++) if(str[i]^48){
int len=i-pre,sum=s[1].sum;
modify(1,0,min(fst,len)-1,(sum+1)%MOD);
modify(1,min(fst,len),len-1,sum);
pre=i;
} printf("%d\n",1ll*s[1].sum*(n-pre+1)%MOD);
return 0;
}

似乎有线性的做法?被微分了/kk

接下来就是大部分题解中提到的线性做法了,考虑什么样的字符串可以被得到,对于一个 \(01\) 串 \(t\),它可以被 \(s\) 得到的充要条件是:

  • \(t\) 第一段 \(0\) 的长度 \(\le\) \(s\) 第一段 \(0\) 的长度
  • \(t\) 最后一段 \(0\) 的长度 \(\le\) \(s\) 最后一段 \(0\) 的长度
  • 假设 \(t\) 中间有 \(y\) 段 \(0\),长度分别为 \(g_1,g_2,\cdots,g_y\),\(s\) 中间有 \(x\) 段 \(0\),长度分别为 \(f_1,f_2,\cdots,f_x\),则存在 \(1\le p_1\le p_2\le\cdots\le p_y\le x\) 满足 \(g_{i}\le f_{p_i}\)。

前两个条件显然可以轻松判断,但是第三个条件貌似有些棘手,我们考虑用一个“匹配”的思想解释上面的内容,我们在扫描 \(t\) 的过程中维护一个指针 \(p\),表示最小的满足当前 \(t\) 的前缀 \(t[1...i]\) 能够从 \(s[1...j]\) 得到的 \(j\)。我们考虑加入一个字符会对 \(p\) 产生什么影响,若加入一个 \(1\),那么 \(p\) 显然会移动到 \(j\) 下一个 \(1\) 的位置,若加入一个 \(0\),则分三种情况:

  • 若 \(s_p='1'\),那么 \(p\) 会移动到下一个 \(0\) 的位置
  • 若 \(s_p='0'\),且 \(s_{p+1}='0'\),那么 \(p\) 显然会移动到 \(p+1\)
  • 若 \(s_p='0'\),且 \(s_{p+1}='1'\),那么意味着这段 \(0\) 的个数超过了当前指针所在的 \(0\) 的段的长度,也就是说这段 \(0\) 不够用了。我们记当前 \(p\) 所在 \(0\) 段为 \(s[L...R]\),那么我们就找到离当前 \(0\) 段最近的 \(0\) 段 \(s[l...r]\),满足 \(r-l+1>R-L+1\),并将指针移动到 \(l+R-L+1\) 的位置。这个稍微想想即可想明白。

如果在上面的过程中 \(p\) 无法再移动了,则说明无法匹配,否则可以匹配。我们考虑以此为 \(dp\) 状态进行转移。我们先预处理出 \(nxt_{i,j}\) 表示当前指针在 \(i\) 处,新加入一个字符 \(j\) 后指针会移动到哪儿,这显然可以单调栈求出。然后设 \(dp_{i,j}\) 表示当前指针在 \(i\) 处,最后一位为 \(j\) 的方案数,枚举下一位的值 \(k\) 然后转移到 \(dp_{nxt_{i,k},k}\) 即可。最后我们强制令最后一位为 \(1\),即统计所有 \(dp_{i,1}\) 的和,再乘上 \(s\) 中第一段 \(0\) 的长度和最后一段 \(0\) 的长度即可。

时间复杂度线性。

const int MAXN=1e6;
const int MOD=1e9+7;
char s[MAXN+5];pii seg[MAXN+5];int pcnt=0;
int n,nxt[MAXN+5][2],dp[MAXN+5][2];
void add(int &x,int y){((x+=y)>=MOD)&&(x-=MOD);}
int main(){
scanf("%s",s+1);n=strlen(s+1);int fst=0,lst=0;
for(int i=1;i<=n;i++) if(s[i]^48){fst=i;break;}
for(int i=n;i;i--) if(s[i]^48){lst=i;break;}
if(!fst) return printf("%d\n",n),0;
for(int l=1,r;l<=n;l=r+1){
r=l;if(s[l]^48) continue;
while(s[r]^49&&r<=n) r++;seg[++pcnt]=mp(r-1,r-l);
} int pre=n+1;
for(int i=n;i;i--){nxt[i][1]=pre;if(s[i]^48) pre=i;}
pre=n+1;
for(int i=n;i;i--){
if(s[i]^49) pre=i;
else nxt[i][0]=pre;
}
for(int i=1;i<=n;i++) if((s[i]^49)&&(s[i+1]^49))
nxt[i][0]=i+1;
stack<pii> stk;
for(int i=pcnt;i;i--){
while(!stk.empty()&&stk.top().se<=seg[i].se) stk.pop();
if(!stk.empty()) nxt[seg[i].fi][0]=stk.top().fi-stk.top().se+1+seg[i].se;
else nxt[seg[i].fi][0]=n+1;stk.push(seg[i]);
}
// for(int i=1;i<=n;i++) printf("%d %d\n",nxt[i][0],nxt[i][1]);
dp[fst][1]=1;int ans=0;
for(int i=fst;i<=n;i++) for(int j=0;j<2;j++){
for(int k=0;k<2;k++) if(nxt[i][k]<=n) add(dp[nxt[i][k]][k],dp[i][j]);
if(j==1) add(ans,dp[i][j]);
} printf("%d\n",1ll*ans*fst%MOD*(n-lst+1)%MOD);
return 0;
}

Codeforces 1383E - Strange Operation(线段树优化 DP or 单调栈+DP)的更多相关文章

  1. [Codeforces 1197E]Culture Code(线段树优化建图+DAG上最短路)

    [Codeforces 1197E]Culture Code(线段树优化建图+DAG上最短路) 题面 有n个空心物品,每个物品有外部体积\(out_i\)和内部体积\(in_i\),如果\(in_i& ...

  2. Codeforces 1107G Vasya and Maximum Profit 线段树最大子段和 + 单调栈

    Codeforces 1107G 线段树最大子段和 + 单调栈 G. Vasya and Maximum Profit Description: Vasya got really tired of t ...

  3. CodeForces 834D The Bakery(线段树优化DP)

    Some time ago Slastyona the Sweetmaid decided to open her own bakery! She bought required ingredient ...

  4. CodeForces 558E(计数排序+线段树优化)

    题意:一个长度为n的字符串(只包含26个小字母)有q次操作 对于每次操作 给一个区间 和k k为1把该区间的字符不降序排序 k为0把该区间的字符不升序排序 求q次操作后所得字符串 思路: 该题数据规模 ...

  5. CodeForces 786B Legacy(线段树优化建图+最短路)

    [题目链接] http://codeforces.com/problemset/problem/786/B [题目大意] 给出一些星球,现在有一些传送枪,可以从一个星球到另一个星球, 从一个星球到另一 ...

  6. Codeforces 786B Legacy(线段树优化建图)

    题目链接  Legacy 首先对于输入的$n$,建立一棵线段树. 显然线段树有大概$2n$个结点,每个节点对应一段区间 我们把这$2n$个结点加入我们的无向图中,一起跑最短路. 具体连边方案: 我们把 ...

  7. D - The Bakery CodeForces - 834D 线段树优化dp···

    D - The Bakery CodeForces - 834D 这个题目好难啊,我理解了好久,都没有怎么理解好, 这种线段树优化dp,感觉还是很难的. 直接说思路吧,说不清楚就看代码吧. 这个题目转 ...

  8. Codeforces 1603D - Artistic Partition(莫反+线段树优化 dp)

    Codeforces 题面传送门 & 洛谷题面传送门 学 whk 时比较无聊开了道题做做发现是道神题( 介绍一种不太一样的做法,不观察出决策单调性也可以做. 首先一个很 trivial 的 o ...

  9. Codeforces Round #426 (Div. 2) D 线段树优化dp

    D. The Bakery time limit per test 2.5 seconds memory limit per test 256 megabytes input standard inp ...

随机推荐

  1. 个人记录:对于python学习的反思和总结(一)

    在写代码时,总是遇到写着写着不知道怎么写了的情况,或者无法把自己的想法用程序表达出来,所以有时候我们需要建立一个自己的编程思路,对一个具体程序的编程有一个比较清晰的想法:因此我把自己的思路总结了一下, ...

  2. python3去除行号

    问题:在复制一些代码时会同时复制每行的行号,删除比较麻烦,所以利用python3本身的代码进行一键删除. # 导入re 模块来使用正则表达式 import re """去 ...

  3. 【Takin使用日记】记一次TransmittableThreadLocal引起的业务异常

    对于常见的 WEB 容器,Takin 通过增强 org.apache.catalina.core.StandardHostValve#invoke 方法,拦截并解析方法入参的 Request 对象中的 ...

  4. CentOS 文件管理

    目录 目录管理 目录结构 切换目录 查看目录 创建目录 复制目录 剪切目录 删除目录 文件管理 查看文件 创建文件 复制文件 剪切文件 删除文件 创建链接 目录管理 目录也是一种文件. 蓝色目录,绿色 ...

  5. 计算机网络之网络层移动IP

    文章转自:https://blog.csdn.net/weixin_43914604/article/details/105319753 学习课程:<2019王道考研计算机网络> 学习目的 ...

  6. Go语言核心36讲(Go语言进阶技术十)--学习笔记

    16 | go语句及其执行规则(上) 我们已经知道,通道(也就是 channel)类型的值,可以被用来以通讯的方式共享数据.更具体地说,它一般被用来在不同的 goroutine 之间传递数据.那么 g ...

  7. 面试官:JavaScript如何实现数组拍平(扁平化)方法?

    面试官:JavaScript如何实现数组拍平(扁平化)方法? 1 什么叫数组拍平? 概念很简单,意思是将一个"多维"数组降维,比如: // 原数组是一个"三维" ...

  8. python 处理xml 数据

    1 import xml.sax 2 import xml.sax.handler 3 4 # python 处理xml 数据 类,将xml数据转化为字典 5 ''' 6 原数据:<?xml v ...

  9. Linux 文本三剑客之 sed

    Linux 系统中一切皆文件. 文件是个文本.可以读.可以写,如果是二进制文件,还能执行. 在使用Linux的时候,大都是要和各式各样文件打交道.熟悉文本的读取.编辑.筛选就是linux系统管理员的必 ...

  10. anaconda无法launch应用(无法l打开任何应用)的问题解决 (点击应用无反应)

    遇到了anaconda 无法launch 任何应用. 重装也不行. 先说我最终的解决方法(在官方文档中找到): 1. 启动 anaconda prompt , 输入 conda remove anac ...