[jzoj]1262. 为奶牛熄灯(迭代加深搜索+一堆优化vs记忆化vs状压DP)

来源:互联网 发布:戴予桐 直播软件 编辑:程序博客网 时间:2024/06/03 17:44

Description

奶牛们喜欢在黑暗的环境里睡觉。当她们每晚回到牛棚准备睡觉时,牛棚里有L(3<=L<=50)盏灯仍然亮着。所有灯的开关按编号升序排成一列,最左边的那个开关控制1号灯(所谓控制,也就是如果1号灯现在亮着,那么按这个开关会使1号灯熄灭,否则这个操作会使1号灯被点亮)。由于奶牛们的蹄子过于粗大,没法方便地按开关,她们总是用一个特制的干草叉来进行对开关的操作。这个叉子设计了T(1<=T<=7)个叉尖,相邻叉尖的距离正好与相邻开关的距离相等。但是现在有些叉尖被折断了。比如说,T=4的一个干草叉,它的第3根叉尖被折断了,我们就用’1101’来描述它。
如果把这个叉子的最左端对准那一列开关的最左端,按下,那1号、2号和4号灯的状态会被改变(3号灯的状态不变,因为那个叉尖被折断了)。在进行这样的操作的时候,任何一个叉尖都必须有一个对应的开关,也就是说,叉子的边缘不能在那一列开关的范围外,即使边缘处的叉尖已经被折断。
现在,你已经知道了各个灯的状态,以及干草叉现在的情况,请你找出一个操作序列,使得在所有操作完成之后,仍然亮着的灯的数目最少。

Input

第1行: 两个用空格隔开的整数:L 和 T
第2行: 一个长度为L的字符串,串中不含空格且元素均为’0’或’1’。第i个元素是’1’则表示第i盏灯亮着,是’0’的话就表示第i盏灯已经被关掉
第3行: 一个长度为T的字符串,只含’0’或’1’(同样不含空格)。如果第i个元素是’1’,说明干草叉的第i根叉尖仍完好无损,否则说明第i根叉尖已经被折断

Output

第1行: 输出一个正整数K,即为了达到目的一共需要用叉子按多少次开关

Sample Input

10 4
1111111111
1101

Sample Output

5

Data Constraint

Hint

【样例说明】
所有的10盏灯都开着。奶牛们使用的干草叉有4个齿,其中第3个齿已经被折断了。
1111111111 开始
1100101111 操作 3(即为把叉子的第一个齿对准第3个开关,按下)
0001101111 操作 1
0000000111 操作 4
0000001010 操作 7
0000010000 操作 6
最后,有1盏灯仍然亮着。这是借助这个干草叉所能得到的最佳结果。当然,可行操作还有很多(最后剩下的灯可能不同)。

Preface

因为T<=7,这一题本质是一个状压DP.

但因为我考场上时脑抽打了个DFS,后来看题解说dfs也能过,于是,我就下定决心,要把这题用搜索切了,再打dp,于是就有了下面这些东西.

Solution

搜索

Method1.0

考虑用n!的效率枚举操作,时间复杂度O(n!).

Method1.5

在Method1.0上,我们知道操作是可逆的,即无论操作顺序,最终得到的状态是一样的.

所以我们只考虑对于每一个位置是否进行操作.

用一个字符串储存状态,每次暴力更新,时间复杂度O(2n).

#include <iostream>#include <cstring>#include <cstdio>using namespace std;char st[52],ch[52];int L,T,i,tot,mmin,ans;void dfs(int k,int sum,int total,int bz){       if (sum<mmin)    {        mmin=sum;        ans=total;    }    if (sum==mmin) ans=min(ans,total);    if (k>L-T+1) return;    tot=0;    for (int j=1;j<=T;j++)        if (ch[j]=='1'){                if (st[j+k-1]=='1') {                                       st[j+k-1]='0';                    tot--;                              } else                {                                       st[j+k-1]='1';                    tot++;                                  }        }    dfs(k+1,sum+tot,total+1,1);    for (int j=1;j<=T;j++)        if (ch[j]=='1'){            if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';        }    dfs(k+1,sum,total,0);}int main(){    freopen("xlite.in","r",stdin);    freopen("xlite.out","w",stdout);    scanf("%d%d",&L,&T);    scanf("%s",st+1);    scanf("%s",ch+1);    tot=0;    for (i=1;i<=L;i++)        if (st[i]=='1') tot++;    mmin=1e8;    ans=1e8;    dfs(1,tot,0,1);    printf("%d\n",ans);}

这里写图片描述


Method1.5

发现上面这个cx打的很不优美

因为是选和不选,最后统计1的个数其实是一样的,不需要每次都统计,这样常数会大.

因为没有加剪枝,所以这样每次统计的效率反而低了.

#include <iostream>#include <cstring>#include <cstdio>using namespace std;char st[52],ch[52];int L,T,i,tot,mmin,ans,sum;void dfs(int k,int total,int bz){       if (k>L-T+1)     {        sum=0;        for (int j=1;j<=L;j++)            if (st[j]=='1') sum++;        if (sum<mmin)        {            mmin=sum;            ans=total;        }        if (sum==mmin) ans=min(ans,total);          return;    }    tot=0;    for (int j=1;j<=T;j++)        if (ch[j]=='1'){            if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';        }    dfs(k+1,total+1,1);    for (int j=1;j<=T;j++)        if (ch[j]=='1'){            if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';        }    dfs(k+1,total,0);}int main(){    freopen("xlite.in","r",stdin);    freopen("xlite.out","w",stdout);    scanf("%d%d",&L,&T);    scanf("%s",st+1);    scanf("%s",ch+1);    mmin=1e8;    ans=1e8;    dfs(1,0,1);    printf("%d\n",ans);}

这里写图片描述


Method2.0(错误方法)

考场上时,想了个不靠谱的记忆化.

f[i][j]表示当前做到第i位,1的个数为j的最少操作数.

这个记忆化分明是对的呀?因为前i位对后面的决策丝毫没有影响,在1的个数相同的情况下,当然操作数越少越好啊.

但是智障的在于,我的1的个数计算的是整个字符串的,于是就Wa了.

因为刚打C++,不会random(尬),自己手出的数据点错了,于是就加多了一维表示第i位是否选了,然后出了十几个数据点发现都没错,就自信的估了个70分.

最后还是尬,这一题也告诉了我对拍是有多么重要.

虽然最后还是多水了一个点.

这里写图片描述

附:Wrongans三个点,T了三个点.


Method2.5(记忆化)

那如果我处理的是前i个出现1的个数呢?

会出现怎样的情况?

这里写图片描述

还是会WA?

为什么?

原来,上面所说的“这个记忆化分明是对的呀?因为前i位对后面的决策丝毫没有影响,在1的个数相同的情况下,当然操作数越少越好啊.”中的“丝毫没有影响”

还是有影响的, 且影响有T个,所以,我们还要再统计出从i1i+T1的状态.

这样就可以记忆化了.

#include <iostream>#include <cstring>#include <cstdio>using namespace std;char st[52],ch[52];int L,T,i,mmin,ans,sum,tot,have;short int f[51][129][51];void dfs(int k,int total,int rec){       if (k>L-T+1)     {        sum=0;        for (int j=1;j<=L;j++)            if (st[j]=='1') sum++;        if (sum<mmin)        {            mmin=sum;            ans=total;        }        if (sum==mmin) ans=min(ans,total);          return;    }    if (k>1)    {        have=0;        for (int j=k+T-2;j>=k-1;j--)            if (st[j]=='1') have=have*2+1; else have=have*2;        if (total<f[k-1][have][rec]) f[k-1][have][rec]=total; else             return;    }    for (int j=1;j<=T;j++)        if (ch[j]=='1'){            if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';        }    tot=0;    if (st[k]=='1') tot++;    dfs(k+1,total+1,rec+tot);    for (int j=1;j<=T;j++)        if (ch[j]=='1'){            if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';        }    tot=0;    if (st[k]=='1') tot++;      dfs(k+1,total,rec+tot);}int main(){    freopen("xlite.in","r",stdin);    freopen("xlite.out","w",stdout);    scanf("%d%d",&L,&T);    scanf("%s",st+1);    scanf("%s",ch+1);    mmin=1e8;    ans=1e8;    memset(f,30,sizeof(f));    dfs(1,0,0);    printf("%d\n",ans);}

这里写图片描述

Method3.0

我们加上一些非常容易想到的剪枝,即如果当前无法改变的‘1’的个数已经大于最优解了,直接return.

并且我们不需要在更新最优解时统计1的个数,我们可以边算边统计.

#include <iostream>#include <cstring>#include <cstdio>using namespace std;char st[52],ch[52];int L,T,i,mmin,ans,sum,tot,have;short int f[51][129][51];void dfs(int k,int total,int rec){       if (rec>mmin) return;    if ((rec==mmin)&&(total>ans)) return;    if (k>L)     {         if (rec<mmin)         {            mmin=rec;            ans=total;        }        if (rec==mmin) ans=min(ans,total);          return;    }    if (k<=L-T+1)    {        if (k>1)        {            have=0;            for (int j=k+T-2;j>=k-1;j--)                if (st[j]=='1') have=have*2+1; else have=have*2;            if (total<f[k-1][have][rec]) f[k-1][have][rec]=total; else return;        }        for (int j=1;j<=T;j++)            if (ch[j]=='1'){                if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';            }               if (st[k]=='1') tot=1; else tot=0;        dfs(k+1,total+1,rec+tot);        for (int j=1;j<=T;j++)            if (ch[j]=='1'){                if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';            }    }    if (st[k]=='1') tot=1; else tot=0;      dfs(k+1,total,rec+tot);}int main(){    freopen("xlite.in","r",stdin);    freopen("xlite.out","w",stdout);    scanf("%d%d",&L,&T);    scanf("%s",st+1);    scanf("%s",ch+1);    mmin=1e8;    ans=1e8;    memset(f,30,sizeof(f));    dfs(1,0,0);    printf("%d\n",ans);}

这里写图片描述

Method4.0

运用迭代加深搜索的思想,我们限定一个‘1’个数的上限,如果超过了直接return,这样在最优解总‘1’个数较少的情况下,这种方法的优化程度较大.

但因为这里已经加了记忆化,所以很多不必要的操作已经return了,然后再枚举‘1’的个数,反而有点多余,但是这毕竟是一种思路,一种很牛逼的思路.

新套路get.

#include <iostream>#include <cstring>#include <cstdio>using namespace std;char st[52],ch[52];int L,T,i,mmin,ans,sum,tot,have,tottot;short int f[51][129][51];void dfs(int k,int total,int rec){       if (rec>mmin) return;    if ((rec==mmin)&&(total>ans)) return;    if (k>L)     {         if (rec==mmin) ans=min(ans,total);        return;    }    if (k<=L-T+1)    {        if (k>1)        {            have=0;            for (int j=k+T-2;j>=k-1;j--)                if (st[j]=='1') have=have*2+1; else have=have*2;            if (total<f[k-1][have][rec]) f[k-1][have][rec]=total; else return;        }        for (int j=1;j<=T;j++)            if (ch[j]=='1'){                if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';            }               if (st[k]=='1') tot=1; else tot=0;        dfs(k+1,total+1,rec+tot);        for (int j=1;j<=T;j++)            if (ch[j]=='1'){                if (st[j+k-1]=='1') st[j+k-1]='0'; else st[j+k-1]='1';            }    }    if (st[k]=='1') tot=1; else tot=0;      dfs(k+1,total,rec+tot);}int main(){    freopen("xlite.in","r",stdin);    freopen("xlite.out","w",stdout);    scanf("%d%d",&L,&T);    scanf("%s",st+1);    scanf("%s",ch+1);    tottot=0;    for (i=1;i<=L;i++)        if (st[i]=='1') tottot++;    for (mmin=0;mmin<=tottot;mmin++)    {        memset(f,30,sizeof(f));             ans=1e8;        dfs(1,0,0);        if (ans<1e8) break;    }    printf("%d\n",ans);}

这里写图片描述

考场上时打的记忆化,是为了节省时间去做后边的题,数据范围不大,记忆化有时并不比dp慢,但因为长年打DP,以至于记忆化都生疏了,其实记忆化也是DP的思想,如果连记忆化都不会,还打什么DP.


状压DP

此题还可运用状压DP求解.

转化一下思路,与记忆化不能完全一样,否则无法转移.

f[i][j][k]表示做到第i位,iT+1i的状态为j,一共有k次操作的最少1的个数.

这样设就可以从i>i+1转移了,判断第i位是否进行操作即可.


此题的一些小技巧

其实我们的dfs还可以用位运算优化,但是由于我加了记忆化,所以位运算起不到什么用处.

但是还是有思想可以借鉴的.

如我要算出前127个数中每一个数转成2进制出现1的个数.

可以直接通过

for (int i=1;i<=127;i++)    f[i]=f[i>>1]+(i&&1)

得出.

不需要再去暴力或者dfs之类的了.

原创粉丝点击