现在的位置: 首页 > 综合 > 正文

[CF 279D]The Minimum Number of Variables[状压dp]

2018年03月17日 ⁄ 综合 ⁄ 共 2550字 ⁄ 字号 评论关闭

题意:

很不好描述的题意...看了Hint再看Description才理解的.

You've got a positive integer sequence a1, a2, ..., an. All numbers in the sequence are distinct.
Let's fix the set of variables b1, b2, ..., bm. Initially each variable
bi
(1 ≤ i ≤ m) contains the value of zero. Consider the following sequence, consisting of
n operations.

有n个数, 作为一组等式的得数.

有m个变量, 可以视为容器.

The first operation is assigning the value of a1 to some variable
bx
(1 ≤ x ≤ m). Each of the following
n - 1 operations is assigning to some variable
by the value that is equal to the sum of values that are stored in the variables
bi and
bj
(1 ≤ i, j, y ≤ m). At that, the value that is assigned on the
t-th operation, must equal
at
. For each operation numbers
y, i, j are chosen anew.

第一个变量首先赋值为a1, 接下来给其他的变量赋值(其他的变量可以是已经出现过的, 将覆盖原值), 并且需要满足第t个赋值语句的返回值与at.相等.

Your task is to find the minimum number of variables m, such that those variables can help you perform the described sequence of operations.

问题是找出最小的m. 即容器个数.

思路:

用样例模拟了一下, 前几个还好判断, 到后面就需要看看再往后是否需要用到这个数来决定是否覆盖...有点像背包问题, 要合理地调度前面的变量...以免出现误删. 同时又要保证总的变量数最少.

用dp.

dp记录当前状态下需要的最少变量数...

看题解:

http://blog.sina.com.cn/s/blog_6ffc3bde01017l92.html

想法:
   很明显是一个DP题,当看到n<=23时也很容易想到是数位DP,但是如何DP却很让人无从下手。一般DP题就是递推推公式,而公式又是由一些很显然的很关键的规律得到的。
   我们可以从第一个数字开始扫,直到扫到最后一个数字,如何从当前数字扫到下一个数字是我们需要解决的问题。
  我们可以将每个数字中的1理解为需要中继的个数,递归时每次都去掉当前位置的1加上上一位置的1(站在上一状态的角度想一想就可以理解为什么要加上一位置的1了)和形成当前位置的数所需要的位置的1,用|(或运算)可以很巧妙得解决位置重复的问题。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define INF 25
int n,a[25],dp[1<<23];
//m表示状态一共23位. 从右往左数. 哪一位为1则表示至少需要多少个中继.
int fun(int m,int x) //扫描到第x位至少需要多少个中继(x从0开始)
{
    if(dp[m]) return dp[m];
    int c = __builtin_popcount(m),v = 0,mn = INF;//c下一状态需要我保证的中继的个数
    for(int i = 0; i < x; i ++)
    {
        for(int j = 0; j <= i; j ++)
        {
            if(a[i] + a[j] == a[x])//如果这两个结果相加可以得到最终结果(尽可能靠前),则...
            {//应该观察到, 每一个a[i]都对应一个变量b,因为结果要有容器.
            //每一个现存的容器都对应了一个a[i].容器是游离的,未知的,只当需要的时候将它计算进来. 每一步都如此, 就可以保证正确.
                v = fun((m&~(1<<x))|(1<<(x-1))|(1<<i)|(1<<j), x-1);
                //第一项表示消除当前变量;1<<(x-1)表示扫描到了前一位,那么这前一位必然要占一个变量.
                //后两项就表示额外需要保留的中介项.(就这么简单?)
                mn = min(mn,max(v,c));//其实所有的数字都是由c得到的,v只作为中介,将不同的c作比较
            }
        }
    }
    return dp[m] = mn;
}
int main()
{
    scanf("%d",&n);
    for(int i = 0; i < n; i ++)
        scanf("%d",&a[i]);
    dp[1] = 1;//初始条件为扫描到第一位..必然至少需要1个变量
    int ans = fun(1<<(n-1),n-1);
    if(ans == 25) ans = -1;
    printf("%d\n",ans);
    return 0;
}

感觉仍是非常难以理解啊......

主要是要培养另一种分析问题的角度.

在本题中,则是:

抓住核心:

每一个得数由两个加数组成, 这两个加数都是之前的某个得数.

如果就选择这两个得数作为加数, 那么这两个得数对应的容器就要保留下来作为中转,而不能被覆盖(复用).

而当前的这个得数需要使用的容器又可以由更靠后的需求决定...

对!还有一点!!容器的编号是不需要考虑的,只要记录所需容器的最大个数,那么就一定够用啦.

m就是有用容器的保留...扫描到不同位时m的变化可看做变量的申请与释放!

本题值得回味啊....

附popcount的一种二分实现...

unsigned popcount (unsigned u)
{
    u = (u & 0x55555555) + ((u >> 1) & 0x55555555);
    u = (u & 0x33333333) + ((u >> 2) & 0x33333333);
    u = (u & 0x0F0F0F0F) + ((u >> 4) & 0x0F0F0F0F);
    u = (u & 0x00FF00FF) + ((u >> 8) & 0x00FF00FF);
    u = (u & 0x0000FFFF) + ((u >> 16) & 0x0000FFFF);
    return u;
}

略神呐><

抱歉!评论已关闭.