【二分+状压dp】Codeforces Round #384 (Div. 2) E. Vladik and cards

【二分+状压dp】Codeforces Round #384 (Div. 2) E. Vladik and cards

【vj链接】


题目

Vladik was bored on his way home and decided to play the following game. He took n cards and put them in a row in front of himself. Every card has a positive integer number not exceeding 8 written on it. He decided to find the longest subsequence of cards which satisfies the following conditions:

the number of occurrences of each number from 1 to 8 in the subsequence doesn’t differ by more then 1 from the number of occurrences of any other number. Formally, if there are ck cards with number k on them in the subsequence, than for all pairs of integers the condition |ci - cj| ≤ 1 must hold.
if there is at least one card with number x on it in the subsequence, then all cards with number x in this subsequence must form a continuous segment in it (but not necessarily a continuous segment in the original sequence). For example, the subsequence [1, 1, 2, 2] satisfies this condition while the subsequence [1, 2, 2, 1] doesn’t. Note that [1, 1, 2, 2] doesn’t satisfy the first condition.
Please help Vladik to find the length of the longest subsequence that satisfies both conditions.

Input
The first line contains single integer n (1 ≤ n ≤ 1000) — the number of cards in Vladik’s sequence.

The second line contains the sequence of n positive integers not exceeding 8 — the description of Vladik’s sequence.

Output
Print single integer — the length of the longest subsequence of Vladik’s sequence that satisfies both conditions.

Examples
Input

3
1 1 1

Output

1

Input

8
8 7 6 5 4 3 2 1

Output

8

Input

24
1 8 1 2 8 2 3 8 3 4 8 4 5 8 5 6 8 6 7 8 7 8 8 8

Output

17

Note

In the first sample all the numbers written on the cards are equal, so you can’t take more than one card, otherwise you’ll violate the first condition.


题目大意与解题思路

题目
给一个长为1000的串(只包含[1,8])
求满足以下条件的最长子序列

 1、相同数字必须连续
 2、所有数字出现次数相差不超过1

比如 [1,2,2,1] 不满足条件1,[1,1,2,2] 不满足条件2.
思路
因为相差不超过1 ,会想先确定最终答案出现最少的次数是多少,设为len,那最后子序列里的数字出现次数为 len 或 len+1 。

求len , 如果 len = x 满足条件,那 len = j (j < len) 也都满足条件,这就可以二分枚举len。

枚举时的check(),要判断一个len是否满足条件,一个想法是枚举[1,8]的全排列,加个预处理

int pre[maxn][9];///pre[i][j]表示前i位有多少个j
int pre2[maxn][9];///pre2[i][j]表示第i个j的位置

这样对每个排列判断需要做八次,但是全排列是O(n!)的复杂度,这个想法很危险。。。

换个想法,我们只需要判断len满不满足条件,数字又只有8,还是连续的,那就有一个状态是连着的len个一样的数,如果每次都连续取len个,那就可以用01表示有没有取过,考虑状压dp。

用dp[i][j]表示前i个里是否有状态j 代表的子序列。
状态 j 的表示方式是其二进制第k位为1则子序列中认为数字k出现了len次。
如果dp[i-1][j]==true;显然dp[i][j]=true;
枚举k=[1,8],如果状态j第k位为1,那它就可以从状态j-(1<<(k-1))转移过来,相当于枚举该状态的最后一个数是k,对应的i的上一位可以通过pre[][]和pre2[][] O(1)的得到,就是最后len个k的前一位。
只要有一个状态可以成功转移就是true;

得到len以后还要求最长长度,这时可以在状态里再加一维表示有多少个数字长度为len+1,即

bool dp[maxn][sum][9];
///dp[i][j][k]表示前i位是否能凑出sum对应数字有len或len+1个,且len+1的有k个

于是状态转移的上一状态变成了k相同,最后一个数字取len个和k-1,最后一个数字取len+1个。

(虽然感觉状态挺多的,但实际运行的时候跑的还挺快,93ms)


AC代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=1007;
const int sum=1<<8;

bool dp[maxn][sum][9];///dp[i][j][k]表示前i位是否能凑出sum对应数字有len或len+1个,且len+1的有k个
bool dp2[maxn][sum];///dp2[i][j]表示前i位是否能凑出sum对应数字有len个的情况
int a[maxn],n;
int pre[maxn][9];///pre[i][j]表示前i位有多少个j
int pre2[maxn][9];///pre2[i][j]表示第i个j的位置
int cnt[9];///记录数字i的数量
bool is_ok(int len)
{
    //printf("len=%d sum=%d\n",len,sum);
    if(len==0) return true;
    int i,j,k,x,y;
    dp2[0][0]=1;
    for(i=1;i<=n;i++)
    {
        dp2[i][0]=1;
        for(j=1;j<sum;j++)
        {
            if(dp2[i-1][j]) {dp2[i][j]=1;continue;}
            dp2[i][j]=0;
            for(k=0;k<8;k++)
            {
                if((j&(1<<k))>0)
                {
                    x=j-(1<<k);///x对应j的上一状态
                    y=pre[i][k+1];
                    if(y>=len)
                    {
                        y=pre2[y-len+1][k+1]-1;///y是取len个k+1后的上一位置
                        if(y>=0&&dp2[y][x]) {dp2[i][j]=1;break;}
                    }
                }
            }
            //if(dp2[i][j]) printf("dp2[%d][%d] ",i,j);
        }//printf("\n");
    }
    return dp2[n][sum-1];
}
int get_ans(int len)
{
    int i,j,k,x,y,z;
    for(i=0;i<=n;i++)
        for(j=0;j<sum;j++)
            for(k=0;k<=8;k++)
                dp[i][j][k]=false;
    dp[0][0][0]=1;
    for(i=1;i<=n;i++)
    {
        dp[i][0][0]=1;
        for(j=1;j<sum;j++)
        {
            x=j;
            k=0;
            while(x>0)
            {
                if(x&1) k++;
                x>>=1;
            }
            for(;k>=0;k--)
            {
                if(dp[i-1][j][k]) {dp[i][j][k]=1;continue;}
                for(x=0;x<8;x++)
                {
                    if((j&(1<<x))>0)
                    {
                        y=j-(1<<x);
                        z=pre[i][x+1];
                        if(z>=len)
                        {
                            //if(i==1&&j==2&&k==1) printf("z=%d pre2[z-len+1][x+1]=%d y=%d k=%d\n",z,pre2[z-len+1][x+1],y,k);
                            if(pre2[z-len+1][x+1]>=1&&dp[pre2[z-len+1][x+1]-1][y][k]) {printf("");dp[i][j][k]=1;break;}
                        }
                        if(z>len)
                        {
                            if(pre2[z-len][x+1]>=1&&dp[pre2[z-len][x+1]-1][y][k-1]) {printf("");dp[i][j][k]=1;break;}
                        }
                    }

                }
                //if(dp[i][j][k]) printf("(%d,%d,%d)",i,j,k);
            }
        }//printf("\n");
    }
    for(i=7;i>=0;i--)
    {
        if(dp[n][sum-1][i]) return i;
    }
}
int main()
{
    int m,i,j,k,x,y;
    scanf("%d",&n);
    for(i=1;i<=n;i++) scanf("%d",&a[i]);
    memset(pre2,-1,sizeof(pre2));
    for(i=1;i<=n;i++)
    {
        pre[i][a[i]]++;
        cnt[a[i]]++;
        pre2[cnt[a[i]]][a[i]]=i;
        for(j=1;j<=8;j++) pre[i][j]+=pre[i-1][j];
    }
    int l,r,mid;
    l=0,r=1007;
    for(i=1;i<=8;i++) r=min(r,cnt[i]);
    r++;
    while(r-l>1)
    {
        mid=(l+r)/2;
        if(is_ok(mid)) l=mid;
        else r=mid;
    }
    //printf("l=%d\n",l);
    if(l==0)///最小长度为0时,get_ans()函数里求取了len个的上一位置要特判,懒得处理就单独算了
    {
        x=0;
        for(i=1;i<=8;i++) if(cnt[i]>0) x++;
        printf("%d\n",x);
    }
    else printf("%d\n",l*8+get_ans(l));
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值