Bestcoder#92Girls Love 233(dp)

问题描述
除了翘课以外,结识新的妹子也是呃喵重要的日程安排之一。
这不,呃喵又混进了一个叫做ACgirls的女生群里,来达成自己不可描述的目的。
然而,呃喵只会喵了个咪地说话,于是很容易引起注意。为了掩饰自己的真实身份,呃喵每次说话都小心翼翼。
她知道,很多女生都喜欢说"233",然而呃喵想说的话一开始就确定好了,所以她要对这句话做修改。
这句话的长度为n,语句里的字符不是'2'就是'3'。
呃喵的智力非常有限,只有m点。她每次操作可以交换两个相邻的字符,然而代价是智力-2。
现在问你,在使得自己智力不降为负数的条件下,呃喵最多能使这个字符串中有多少个子串"233"呢?
如"2333"中有一个"233","232323"中没有"233"
输入描述
第一行为一个整数T,代表数据组数。
接下来,对于每组数据——
第一行两个整数n和m,分别表示呃喵说的字符串长度 以及呃喵的智力
第二行一个字符串s,代表呃喵具体所说的话。

数据保证——
1 <= T <= 1000
对于99%的数据,1 <= n <= 10, 0 <= m <= 20
对于100%的数据,1 <= n <= 100, 0 <= m <= 100
输出描述
对于每组数据,输出一行。
该行有1个整数,表示最多可使得该字符串中有多少个"233"
输入样例
3
6 2
233323
6 1
233323
7 4
2223333
输出样例
2
1
2

出题人的分析:

大家要学会分析状态啊喂!多思考多开脑洞,分析出状态之后,就是一个DP或者记忆化搜索,自然就可以写出来啦!

首先,因为字符不是'2'就是'3',所以我们可以把字符串当做一个全部都是'3'的串,然后有若干的'2'插入到了某些位置。

显然,我们交换相邻的'2'与'2'或者相邻的'3'与'3'是没有意义的,我们只会进行相邻'2'与'3'之间的交换。因此,所有'2'的相对前后关系其实是不会变化的。

做了这些比较基础的分析之后,基于数据规模很小,我们可以用以下4个要素表示完整的状态:

1.处理到第几个'2'

2.最后一个'2'停留在什么位置,如果当前的'2'与上一个'2'距离相差>=2时则对答案+1

3.呃喵的剩余交换次数是多少

4.当前已经成功得到几个"233"

而这四个要素的大小,最坏情况下分别是n、n、m、n级别的数,我们随便以3个要素作为下标对应状态,使得第4个要素最优做DP. 转移的时候步长也是不超过2m的,所以很容易就可以得出复杂度为O(n * n * m/2 * m)的算法,这个对于本题的时限和数据,没有什么作死写法的话是可以完全无压力顺利AC的。

PS:由于数据组数过多,直接memset复杂度是1e6*1000级别,会导致TLE哦~ 其实初测已经给了很多组数,目的就是让memset的代码意识到其可能会FST.

出题人博客的分析:
【分析】
这道题有些难于思考。
一开始不妨使得m/=2,这时的m就是我们的最大交换次数。
首先我们可以得到一个贪心原则:我们肯定不会改变所有给定'2'的先后顺序,即我们只会交换相邻'2'和'3'之间的位置。
于是,我们预处理出p[i]表示第i个'2'的位置为p[i]。
而且,我们应该观察到,在这道题中,m非常小,这个可以作为我们的突破口,因为每个'2'就算经过交换,距初始位置p[]的距离也不会超过m。


在这个基础上,我们构想一个DP:
用f[i][j][k]表示――
我们已经处理了从左到右数的第i个'2',已经用了j次交换,且第i个'2'的最终位置k,在这种状态下的最多的"233"的个数。
那么我们有一个DP转移方程――
f[i][lj+j][p[i]±j]=max(f[i-1][lj][lp(all possible positions)] + (p[i]±j - lp >= 3), p[i]±j > lp
意思是――
我们处理了前i个'2'的位置,也一定是通过前i-1个'2'的位置的状态所转移而来的。
然后处理前i-1个'2'的位置时,用了lj次交换,现在这第i个'2',我们额外用了j次交换,
第i-1个'2'的合法位置范围,是一个很小的区间范围(不会超过2m),这里暴力枚举即可。
然而,因为要保证我们贪心原则的正确性,当前'2'的位置必须严格比前一个大,于是p[i]±j > lp。
然后,如果相邻的两个'2'的距离>=3,那么我们便会多出一个233。

我的分析及收获:
  1. 如可以把所有的3看做是固定的,题目只是要求在有限的次数下移动2,是的构成的233最多。下面假设每个不同的2只能移动一次。比如33333322,首先把第一个往右移动两位就成了33332332,难道你现在看的出我刚才移动的到底是第一个2还是第二个2么,肯定是看不出来的吧。那么再把最后一个2向前移动构成了33233233,很明显这样做是先把第一个2向前移动4位构成33233332,然后第二个2向前移动2位构成33233233来的划算。因为2都长得一模一样,即使你第一次移动的2是错误的决定(把第一个2向前移动了两位),那么也可以把第二个2也移动到这个位置上来,然后把第一个2继续向前移动2位就行,相当于同一个2可以移动多次了。只要能get到,怎么理解都行,关键是要想!
  2. 在上面的分析后,就可以枚举第一个2,如果爆搜的话,就是枚举到可以移动到哪些位置,固定下来后,枚举第二个2。当然在基于第一条分析之后,在枚举第二个2放置的位置时,只需要从第一个2后面的位置开始枚举就行,这才是符合贪心策略的。
  3. 有了上面分析后,就是把爆搜转化为dp的问题了。
  4. 这个题还有一个点就是出题人提到的memset超时,所以可以用一个时间戳,来避免初始化dp数组造成的超时!第一次get到这个技能!
代码中有详细注释:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#include<queue>
#include<set>
#include<map>
using namespace std;
#define PI acos(-1.0)
#define eps 1e-8
#define pb push_back
#define mp make_pair
#define MII map<int,int>::iterator
#define MLL map<LL,LL>::iterator
#define pii pair<int,int>
#define SI set<int>::iterator
#define SL set<LL>::iterator
#define MS(x,y) memset(x,y,sizeof(x))
#define MC(x,y) memcpy(x,y,sizeof(x))
#define ls o<<1
#define rs o<<1|1
#define bug printf("bug-------bug-------bug\n")
using namespace std;
typedef long long ll;
typedef unsigned long long ul;
typedef unsigned int UI;
template <class T1, class T2>inline void gmax(T1 &a, T2 b) { if (b>a)a = b; }
template <class T1, class T2>inline void gmin(T1 &a, T2 b) { if (b<a)a = b; }
const int N = 105, Z = 1e9 + 7, ms63 = 0x3f3f3f3f;
int casenum, kase;
int n, m, g;
char s[N];
int p[N];

int dfn[N][N][N];
int dp[N][N][N];
int dfs(int k, int lp, int tim)//k表示处理的是第几个2,lp是上一个2的位置,tim是剩余的移动次数
{
    if(k > g)//移动完了最后一个2,如果它的位置与字符串的末尾之差大于2,就说明有233生成
        return (n-lp >= 2);
    if(dfn[k][lp][tim] == kase)//时间戳,也就是dp中之前已经计算过的状态,直接返回,避免了dp数组的初始化
        return dp[k][lp][tim];
    dfn[k][lp][tim] = kase;//时间戳,标记该种状态也被计算出
    dp[k][lp][tim] = -1e9;
    int l = max(lp+1, p[k]-tim);//当前2能够移动到的最左端
    int r = min(n, p[k]+tim);//当前2能够移动到的最右端
    for(int i = l; i <= r; i++)//枚举当前2能够移动到的位置
    {
        int cost = abs(p[k] - i);//移动的花费
        gmax(dp[k][lp][tim], dfs(k+1, i, tim - cost) + (i - lp >= 3) * (k > 1));
    }
    return dp[k][lp][tim];
}
void solve()
{
    scanf("%d", &casenum);
    for(kase = 1; kase <= casenum; kase++)
    {
        scanf("%d%d", &n, &m);
        m /= 2;
        scanf("%s", s+1);
        g = 0;
        for(int i = 1; i <= n; i++)
            if(s[i] == '2')
                p[++g] = i;
        if(g == 0)
        {
            puts("0");
            continue;
        }
        int ans = dfs(1, 0, m);
        printf("%d\n", ans);
    }
}
int main()
{
    solve();
    return 0;
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值