0.闲话
没错,又是我,至于为什么这段时间我一直没有发文章。
是因为我也是一个蒟蒻,也要备战CSP,然后,就被打蒙了
为了让大家不要信心备受打击,所以发了一篇题解。
1.审题
小 L 现在在玩一个低配版本的消消乐,该版本的游戏是一维的,一次也只能消除两个相邻的元素。
现在,他有一个长度为 n 且仅由小写字母构成的字符串。我们称一个字符串是可消除的,当且仅当可以对这个字符串进行若干次操作,使之成为一个空字符串。
其中每次操作可以从字符串中删除两个相邻的相同字符,操作后剩余字符串会拼接在一起。
小 L 想知道,这个字符串的所有非空连续子串中,有多少个是可消除的。
输入的第一行包含一个正整数 n,表示字符串的长度。
输入的第二行包含一个长度为 n 且仅由小写字母构成的的字符串,表示题目中询问的字符串。
输出一行包含一个整数,表示题目询问的答案。
样例1
8 5
accabccb
2.读懂样例
写题最忌 不读样例就写,我们先来看看有哪5个可消除子串。
它们分别是cc acca cc bccb accabccb
样例告诉了我们什么?
他告诉我们,不同位置的两个相同字串算2个。
两个可消除子串接在一起也是一个可消除子串
3.暴力
众所周知 ,西西弗的数据是值得相信的东西,所以果断用暴力。
3.1极端的暴力
枚举左端点和右端点,然后判断是否可消除。
时间复杂度O(n^3),预期得分35pts!!!
(不愧是希希弗)
代码就不放了
3.2暴力优化
仔细思考,这道题像不像一个字母版的判断一个括号序列是否合法,只不过要枚举左端 点而已。代码如下:
#include<bits/stdc++.h> #define ll long long using namespace std; int n; ll cnt; string s; stack<char> st; int main(){ cin>>n; cin>>s; for(int i=0;i<n;++i){ st.push(s[i]); for(int j=i+1;j<n;++j){ if((!st.empty()) and st.top()==s[j]){ st.push(s[j]);st.pop();st.pop(); } else st.push(s[j]); if(st.empty()){ cnt++;/*cout<<i<<" "<<j<<endl;*/} } while(!st.empty()) st.pop(); } cout<<cnt; return 0; }
50pts!!!
4.DP
检查可消除字串的性质 ,我们发现暴力代码并没有用到性质2,那性质2能做什么呢?
如果字串【i,j】能被消除,那么以j为右端点的可消除字串个数=以i为右端点的可消除字串个数+1
原因:假设字串[k,i]可以被消除,那么根据性质2,[k,j]也能被消除,再加上[i,j]。就是以i为右端点的可消除字串个数+1
这时问题来了,如果从后往前遍历第一个可以使[i,j]成为可消除字串的i,那么其复杂度与暴力优化无异。
好问题!
记g[j]为第一个可以使[i,j]成为可消除字串的i
考虑两种情况:
1.如果[j-1,j]可消除,也就是说c[j-1]==c[j],那么g[j]=j-1;
2.如果不行
我们就要将c[j-1]消除,也就是说考虑c[g[j-1]-1]是否等于c[j];
重复此操作——直到
某个g[x]==-1(无法消除)将g[j]=-1 or
某个c[x]==c[j] 将g[j]=x
那么得到状态转移方程
dp[j]=dp[g[j]]+1
代码实现:
#include<bits/stdc++.h> #define ll long long using namespace std; int n,dp[2000010],g[2000010]; string s; int main(){ cin>>n; cin>>s; for(int i=0;i<n;++i){ char x=s[i]; int ni=i-1; while(1){ if(s[ni]==x) {g[i]=ni;break;} else if(g[ni]==-1 or ni<0){ g[i]=-1;break;} ni=g[ni]-1; } } ll cnt=0; for(int i=0;i<n;++i){ if(g[i]==-1) dp[i]=0; else dp[i]=dp[g[i]-1]+1; cnt+=dp[i]; } cout<<cnt; return 0; }
100pts,AC了
5.祝福
unsigned long long rp=0;
rp--;
再见