[BZOJ2064]-分裂-状压dp思路好题

说在前面

一道训练思维的好题,然而me还没想多久就去看题解了
真是浪费了一道题啊…后悔++


题目

BZOJ2064传送门

题目大意

给出两个数列A,B,它们的长度分别为N,M(数字个数不超过10,数字均在[1,50]内)。A数列的和等于B数列的和。现在有两种操作:
将一个数字拆分成两个数字,这两个数字之和等于原来的那个数字,属于的数列不变(原来是A数列的,拆开之后还是A数列的);
将两个数字合并成一个数字,这两个数字之和就是新的那个数字,属于的数列不变(只能是同一数列的数字合并)
询问最少操作多少次可以将A变成B

输入输出格式

输入格式:
第一行描述A数列,其中第一个数N表示A中数字个数,接下来N个数表示数列A中的数字。
第二行描述B数列,输入B数列的方式与A相同

输出格式:
输出最少操作次数


解法

思路大概是这样的

首先可以确定的是,对于任何一组合法的A,B,A至多需要N+M-2次就可以变换到B,这是显然的。可以先经过N-1次操作把A数列合并成一个数,然后再经过M-1次操作拆分成B数列。

然而这样的步骤中可能有不必要的操作。如果说在把A合并成一坨的过程中,某个时刻这一坨已经和B中某些数字的和相等了,那么完全可以把这一坨直接拆分成B那些数字,而不需要继续和其他的A中的数字合并成更大的一坨再去拆分。
也就是说,如果A中有子集和B的某一个子集和相等,那么完全可以把这两个子集单独处理。每多一个 不相交的 且 和相等的 子集,总步数就会减少2,所以最后答案就是N+M-2*子集个数

而如果A中没有任何一个子集和B中任何一个子集和相等(全集和空集除外),那么无论是边合并边拆分,还是先合并后拆分,它们的步数都是一样的,为|A|+|B|-2。

证明:假设经过i次操作之后(其中 i1 次拆分, i2 次合并),A中所有数字均已小于B(不然继续拆分)。现在将A中的数字不断合并,直到A中有一个数字大于B中某个数字,假设这时合并了 j 次。那么这个数字一定可以拆分成 B中的某个数字+另一个不在B中的数字(如果另一个数字也在B中,那么将A的这个数字加上已经拆分好的数字之和 等于 拆分之后的两个数字加上已经拆分好的数字之和,与假设矛盾)。那么把拆分出的 在B中的那个数字从A和B中同时拿走,剩下的是一个子问题,而已经消耗的步数为i(初始) +j (当前合并) +1 (拆分),而剩下的问题规模A: Ni2j+1 (因为拆分而增多) 1 (因为拿走而减少),B: Mi11 。递归终止时,A和B剩余规模都为1,此时 i1=M2 i2=Nj1 ,而当前层操作所用步数为 j+1 ,所以总步数为 M+N2

具体实现大概是这样的

定义dp[stateA][stateB]表示在A选择的集合为stateA,在B选择的集合为stateB时,有多少不相交的子集相等。
如果当前stateA之和不等于stateB,那么
dp[stateA][stateB]=max(dp[stateA][stateB],dp[stateA][stateB])
如果当前stateA的和等于stateB,从中任意去掉一个元素之后,相等子集个数都会减一,反过来就有
dp[stateA][stateB]=max(dp[stateA][stateB],dp[stateA][stateB])+1


下面是自带大常数的代码

/**************************************************************
    Problem: 2064
    User: Izumihanako
    Language: C++
    Result: Accepted
    Time:840 ms
    Memory:2872 kb
****************************************************************/

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N1 , N2 , a1[15] , a2[15] , Fstate1 , Fstate2 ;
short dp[1024][1024] , sum1[1024] , sum2[1024] ;

void preWork(){
    Fstate1 = ( 1 << N1 ) - 1 ;
    Fstate2 = ( 1 << N2 ) - 1 ;
    short s , j ;
    for( s = 0 ; s <= Fstate1 ; s ++ ){
        for( j = 0 ; j < N1 ; j ++ )
            if( s&(1<<j) ) sum1[s] += a1[j+1] ;
    }
    for( s = 0 ; s <= Fstate2 ; s ++ ){
        for( j = 0 ; j < N2 ; j ++ )
            if( s&(1<<j) ) sum2[s] += a2[j+1] ;
    }
}

void solve(){
    short s1 , s2 , k ;
    for( s1 = 1 ; s1 <= Fstate1 ; s1 ++ ){
        for( s2 = 1 ; s2 <= Fstate2 ; s2 ++ ){
            short tmp = 0 ;
            for( k = 0 ; k < N1 ; k ++ )
                if( s1&(1<<k) ) tmp = max( dp[s1^(1<<k)][s2] , tmp ) ;
            for( k = 0 ; k < N2 ; k ++ )
                if( s2&(1<<k) ) tmp = max( dp[s1][s2^(1<<k)] , tmp ) ;
            dp[s1][s2] = tmp + ( sum1[s1] == sum2[s2] ) ;
        }
    }
    printf( "%d" , N1 + N2 - 2*dp[Fstate1][Fstate2] ) ;
}

int main(){
    scanf( "%d" , &N1 ) ;
    for( int i = 1 ; i <= N1 ; i ++ )
        scanf( "%d" , &a1[i] ) ;
    scanf( "%d" , &N2 ) ;
    for( int i = 1 ; i <= N2 ; i ++ )
        scanf( "%d" , &a2[i] ) ;
    preWork() ;
    solve() ;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值