题目二:
dp(动态规划经典)
leafee 最近爱上了 abb 型语句,比如“叠词词”、“恶心心”
leafee 拿到了一个只含有小写字母的字符串,她想知道有多少个 "abb" 型的子序列?
定义: abb 型字符串满足以下条件:
- 字符串长度为 3 。
- 字符串后两位相同。
- 字符串前两位不同。
#include<bits/stdc++.h>
using namespace std;
long long sum[101010][26];
int main(){
int n,i,j;
string s;
cin>>n>>s;
for(i=n-1;i>=0;i--){
for(j=0;j<26;j++)sum[i][j]=sum[i+1][j];
sum[i][s[i]-'a']++;
}
long long res=0;
for(i=0;i<n;i++){
for(j=0;j<26;j++){
if(j!=s[i]-'a'){
res+=sum[i+1][j]*(sum[i+1][j]-1)/2;
}
}
}
cout<<res;
}
补充:
DP:将一个大的问题,不断拆分为一个个小的问题,小问题和小问题之间是存在联系的。一般我们都可以通过递推关系来去找到它们之间的联系。
它可能是由前面的某些数的组合得到,也有可能是由前面的某些数求得最优解得到,等等。这个时候,其就会和贪心、排列组合等知识组合到了一起。而如何找到这样一个作用方式,正是dp问题的关键。
一般而言,每一种独立的结果我们称之为状态。而我们将作用方式称之为状态转移方程。
理解:
1、拆分子问题;将大问题拆分成小问题。
2、记录状态;每一个子问题得到的状态一般都记录在了一个dp数组当中,或者是通过栈、队列等其他的方式记录。
3、DP无后效性。按照一定顺序去求解拆分的小问题。当小问题的状态或者结果得到了之后,我们就再也不去关心它是怎么样得到的了。
方法:
1、首先我们将一个大的、整体的问题拆开,拆成小的问题。那么我们就要确定,每一个小的问题解决之后所得到的结果表示的是什么。即确定每一个小的问题表示的状态。比如,dp[i]表示前i个数的某种方法的个数,再比如,dp[i][j]表示某个字符串从i到j是不是回文串等等。就是说,你要明白,你的每一张多米诺骨牌代表的是什么东西。
2、确立每一个状态之间的转移关系。多米诺骨牌立好之后,是不是要想着这些骨牌要怎么摆放的问题?那么状态转移就是可以理解为用某种方法,将骨牌摆好。比如,dp[i] = dp[i-1]+dp[i-2],复杂一点的,可能还有类似于dp[i] = max(dp[i],dp[i-dp[j]]) (j = 0, 1, 2...,i - dp[j] >= 0)这样。
3、确立初始状态。就是给多米诺骨牌一个初始的动力,让它可以触发效应,一个一个全部倒下。最最常见的,就比如dp[0] = 1 这样子的。也叫确立边界条件。
指在求解问题当中的遍历顺序。这点和上述的是紧密相关的。也就是说,你要明白你的骨牌是怎样一个个倒下的(是从后往前?还是从前往后?)最典型的,当进行空间优化的时候,就会看出遍历顺序所带来的重要作用。