状压与状态dp

1 篇文章 0 订阅

状压dp

第一题
CF题目链接

B. Little Pony and Harmony Chest

time limit per test4 seconds
memory limit per test256 megabytes
input standard input
output standard output

Princess Twilight went to Celestia and Luna's old castle to research the chest from the Elements of Harmony.
A sequence of positive integers bi is harmony if and only if for every two elements of the sequence their greatest common divisor equals 1. According to an ancient book, the key of the chest is a harmony sequence bi which minimizes the following expression:


You are given sequence ai, help Princess Twilight to find the key.

Input
The first line contains an integer n (1 ≤ n ≤ 100) — the number of elements of the sequences a and b. The next line contains n integers a1, a2, ..., an (1 ≤ ai ≤ 30).

Output
Output the key — sequence bi that minimizes the sum described above. If there are multiple optimal sequences, you can output any of them.

Sample test(s)
input
5
1 1 1 1 1
output
1 1 1 1 1
input
5
1 6 4 2 8
output
1 5 3 1 8

简述一下题目的意思,给你一串数字,将它们变成两两互质的数(两两最大公约数为1),同时要求相对应每个数变化绝对值之和最小
这些数的范围是1到30,一般要用状压dp做的,数字范围都不会太大
首先,这些数如果都变成1,肯定符合要求,所以,对于改动后的每个数,肯定不会大于58,大于58还不如变成1。

1到58的质因子一共有16个,这个可以求,也可以自己直接赋值一下,每个质因子对应二进制的一个1,相当于开个2^16次方大小的空间,使每个质因子的有无与该数字二进制相应点上是1还是0对应,
dp开两维,第一维对应当前第几个数,第二维是现在哪几个位上为(即有哪几个质因子已经存在了)
还要注意一下,要记录好当前选第i个数时,对应每个状态绝对值变化取最小值时j的取值,到时候最后还原的时候倒着递推回来,思路要清晰,最后递推回来下标比较绕。我比较粗心,绕了半天才绕清楚

c++
#include <iostream>
#include <cstdio>
#include <cmath>
#include <queue>
#include <algorithm>
#include <cstring>

using namespace std;
int prim[20] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53};
int a[105];
int state[60];
int ans[105][1<<16];
int ansn[105][1<<16];
int anse[105];
int main()
{
    //freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);

    memset(state, 0, sizeof state);

    for (int i=1; i<=58; i++)
        for (int j=0; j<16; j++)
            if (i%prim[j]==0)
                state[i] |= 1<<j;

    int n;
    scanf("%d", &n);
    for (int i=0; i<n; i++)
    {
        scanf("%d", &a[i]);
    }
    memset(ans[n], 0, sizeof (int)*(1<<16-1));
    for (int i=n-1; i>=0; i--)
    {
        for (int k=0; k<(1<<16); k++)
        {
            ans[i][k] = 100007;
            for (int j=1; j<=58; j++)
            {
                if ((k&state[j])!=0) continue;

                int temp = ans[i+1][k|state[j]] + abs(j-a[i]);
                if (temp<ans[i][k])
                {
                    ans[i][k] = temp;
                    ansn[i][k] = j;
                }
            }
        }
    }
    int ansd;
    ansd = 100007;
    int kill;
    for (int k=0; k<(1<<16); k++)
    {
        if (ans[0][k]<ansd)
        {
            ansd = ans[0][k];
            anse[0] = ansn[0][k];
            kill = k;
        }
    }
    printf("%d", anse[0]);
    int i = 1;
    while (i<n)
    {
        kill = kill|state[anse[i-1]];
        anse[i] = ansn[i][kill];
        printf(" %d", anse[i]);
        i++;
    }
    printf("\n");

    return 0;
}

状压2
boj高兴-----题目链接

  1. 高兴
    时间限制 4000 ms 内存限制 65536 KB
    题目描述
    小弱有n个玩具,现在小弱想把他们排列成一行,把玩具j放在玩具i的右边相邻的位置上,他将得到一个高兴值Hij.

输入格式
输入有多组数据。
每组数据以一个整数n(n <= 18)开头。
接下来n行, 每行n个整数。 第i行第j列Hij( Hij的绝对值 <= 10000)代表把玩具j放在玩具i的右边相邻的位置时高兴值。输入保证Hii = 0,最左边只考虑与右边相邻的玩具,最右边只考虑与左边相邻的玩具。

输出格式
对于每组数据输出最大高兴值。

输入样例
2
0 1
2 0
3
0 -1 7
3 0 3
3 3 0
输出样例
2
10

使用了状态压缩dp,当然递推式有很多种,我直接抄了小白书上的,用递推确实比用递归快了一点,而且内存用得少一些,不过个人感觉递归会比较好理解吧
dp[s][v] 表示的是以v为起点走完不在s内的所有点的一条路(最大或最小)权值。其中v在集合s内。
dp[s][v] = max(dp[s][v] , d[v][u] + dp[s | 1 << u][u] )
这个递推式是从大到小递推的,太难理解了(唉,抄别人的就是不好,要理解很长时间,要是我自己写肯定不会写这么深奥的)
所以对于本题dp[1<<i][i] 表示的是以i为起点走遍所有点的最大权值(显然此时s中只有i点)

c++

#include <iostream>
#include <cstdio>
#include <string.h>
#include <vector>
#include <cmath>
#include <algorithm>
#define INF 1800000
using namespace std;

int n;
int d[20][20];
int dp[1 << 18][20];
bool vis[1 << 18][20];//也可以dp设个初始值,不等于初始值就表示访问过了

int rec(int s, int v)
{
    if (vis[s][v] == true)
        return dp[s][v];
    if (s==((1<<n)-1))
        return dp[s][v] = 0;

    int res = -INF;
    for (int u=0; u<n; u++)
         if (!(s >> u & 1))
            res = max(res, rec(s | 1 << u, u) + d[v][u]);//

    vis[s][v] = true;
    return dp[s][v] = res;
}
int main()
{
    while (~scanf("%d", &n))
    {
        memset(vis, 0, sizeof vis);

        for (int i=0; i<n; i++)
            for (int j=0; j<n; j++)
                scanf("%d", &d[i][j]);

        int ans = -INF;
        for (int i=0; i<n; i++)
            ans = max(ans, rec(1<<i, i));

        printf("%d\n", ans);
    }
    return 0;
}
#include <iostream>
#include <cstdio>
#include <string.h>
#include <vector>
#include <cmath>
#include <algorithm>
#define INF 1800000
using namespace std;

int n;
int d[20][20];
int dp[1 << 18][20];

int main()
{
    while (~scanf("%d", &n))
    {
        for (int i=0; i<n; i++)
            for (int j=0; j<n; j++)
                scanf("%d", &d[i][j]);
        for (int s=0; s < 1<<n; s++)
            fill(dp[s], dp[s]+n, -INF);

        for (int i=0; i<n; i++)
            dp[(1<<n)-1][i] = 0;//加减号的优先级比左移运算符高



        for (int s=(1<<n)-2; s>0; s--)
            for (int v=0; v<n; v++)
                if (s>>v & 1)
                    for (int u=0; u<n; u++)
                        if ((u!=v)&&(!(s>>u&1)))
                            dp[s][v] = max(dp[s][v], dp[s|1<<u][u] + d[v][u]);
        //犯傻了。这里括号写错,主要是抄错了,写成了max(dp[s][v], dp[s|1<<u][u]) + d[v][u];
        //调试得眼睛都花了,才找出来                
        int ans = -INF;
        for (int i=0; i<n; i++)
        {
            ans = max(ans, dp[1<<i][i]);
        }
        printf("%d\n", ans);
    }
    return 0;
}

状压3
题目链接

  1. 学妹去搬砖
    时间限制 1000 ms 内存限制 65536 KB
    题目描述
    大家都知道,学校里有很多路在修,修路需要砖块。这一天,Mr.F来到集训队,找学妹去帮忙搬砖块。善良的学长们不忍心让学妹劳动,就争先恐后的帮助学妹搬砖。于是聪明的学妹说,我出一个题,谁答出来谁就能帮我搬砖。
    把学校要铺的地面看成是nm的方格,每一块砖的大小是12,学妹想知道有多少种方法可以把这块地铺满。注意地上有可能会有花花草草,有爱心的学妹不忍心砖块把它们压死,所以这些点是不可以铺砖块的。

输入格式
输入多组数据,数据组数不超过20组。每组第一行为三个整数n, m, k,(1<=n, m<=10),k<=n*m,分别代表地面的长宽,以及花花草草的数量。接下来k行,每行一组x,y,表示花花草草的坐标。详细方向见样例。

输出格式
每组输出一个数,即最后的方案数。由于输出会很大,请输出答案mod 1000000007(10^9+7)。

输入样例
3 1 1
2 0

3 1 0
输出样例
1
0

hint:
第一组样例所示地面为:
.
.
*
其中‘.‘表示空地,’*‘表示花花草草,一种方案可以铺满。

这也是白书上一道非常经典的题目

如果现在只有一个格子或者两个格子,那么我们放砖块的方法数是很好计算的
所以代码是从最右下角开始往上递推,
假如其余所有格子都放满了,现在要放最后一个,有几种方法
一直到,假如一个格子都没放,现在要从第一个开始放有几种
中间过程就是,假如其余前面格子都放满了,现在要从当前位置开始放一直到放满有几种
虽然1*2的格子我们在考虑放在某个点的时候有四个朝向,但是基于对称原理,我们只考虑往右和往下
因为往上的方向,可以看做上面格子往下,
为了构造连续推导关系,我们需要考虑的是前面都放满了,处理完当前(i, j)点后,我们会转移到接下来的哪个状态
(s是个二进制表示的状态,是一行待定的可能的状态)
s表示的是一行,但是这是特殊的一行,这一行可能不在同一直线上,它们的共同点是它们的上面都必须放满了
如果它是1,表示它被已经排好的格子占用了,(注意它们相互间不占用,带排的这一行没有主动关系),
如果为0,表示没有被占用

当前状态的方法数设为dp[cur][s],下一个状态的方法数为dp[next][s'],显然dp[cur][s]等于所有可能的dp[next][s']的和
两个状态之间的转移其实只涉及到了它自己,它右边,它下面

当前点(i, j)
s中如果这个点是1,表示它被左边或上面的格子占用了,那么它肯定不会占用它下面的格子,下一个状态中,
它下面的点也是j位置是必须是0(0表示没有被占用)才有可能是它转移过去的,

s中如果这个点是0,表示可以自由放1*2格子,可以向右延伸,也可以向下延伸,
向右,那么右边这个格子(j+1)在位置上其实也是下一状态的(j+1), 首先它在当前状态必须是0,表示没被它上面占用
其次在下一状态必须是1,不会再铺,(表示已之前被铺,受限制)
向下,当前肯定是0; 下一状态也必须要为1了。

花其实好考虑的,加点判断进去就好了

#include <iostream>
#include <cstdio>
#include <cmath>
#include <queue>
#include <algorithm>
#include <cstring>
#define maxn 1000000007
using namespace std;
bool a[15][15];
int dp[2][1<<10];//
int main()
{
    //freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);
    int n, m, k;

    while (~scanf("%d %d %d", &n, &m, &k))
    {
        memset(a, 0, sizeof a);
        int tx, ty;
        for (int i=0; i<k; i++)
        {
            scanf("%d %d", &tx, &ty);
            a[tx][ty] = true;
        }
        if ((m*n-k)&1)
        {
            printf("0\n");
            continue;
        }
        int cur = 0;
        int next = 1;
        //memset(dp[next], 0, sizeof (long long)*(1<<m));//一开始出错是因为改了long long却没有改 sizeof (long long) 其实最后发现也不需要用long long
        memset(dp, 0,sizeof dp);
        dp[next][0] = 1;
        for (int i=n-1; i>=0; i--)
            for (int j=m-1; j>=0; j--)
            {
                for (int k=(1<<m)-1; k>=0; k--)//千万注意优先级
                {
                    dp[cur][k] = 0;
                    if ((k>>j&1)||(a[i][j]))
                        dp[cur][k] = dp[next][k&(~(1<<j))];
                    else
                    {
                        if (((j+1)<m)&&!(k>>(j+1)&1)&&(!a[i][j+1]))
                            dp[cur][k] = (dp[cur][k]+dp[next][k|(1<<(j+1))])%maxn;
                        if (!a[i+1][j])
                            dp[cur][k] = (dp[cur][k]+dp[next][k|(1<<j)])%maxn;

                    }
                }
                swap(next, cur);//最后的结果保存在next里面
            }
         printf("%d\n", dp[next][0]);
    }


    return 0;
}

最后这一道不应该叫状压dp,因为本身状态少不涉及压缩

这是个dp的题
对于一个有a个石子的堆,可以等概率地变成1到a其中的任意一个数所以状态转移方程可以写成如下,因为当前转态只涉及自己和前面一个,中间又不需要记录什么数据,直接开个滚动数组就可以了,这样大大减小空间开销,我一开始听说还要用到背包,吓坏了,后来人家又告诉我不用背包,我才敢看题,注意初始条件,变量初始化,中间思路清晰还是不算坑的题
dp[cur][k] += dp[pre][j^k]/a[i];

Study sister's dragon
时间限制 1000 ms 内存限制 65536 KB
题目描述
Study sister plays the game coc everyday. One day, she asked Study brother to donate a dragon into her castle, but Study brother refused because the dragon cost too much.
At last, they decide to play a rock game, and if Study brother lose the game, he would donate a dragon.
There are several piles of rocks, and they pick some rocks from one pile alternatively. According to game theory, it is easy to tell who would win the game. So Study brother made a new rule. Before the game starting, Study brother will pick a random number of rocks(might be zero) but not all of them from each pile, and then they start play the game.
Now Study brother wants to know his chance to win.
As a gentleman, Study brother alway let Study sister to pick first when they play the game.
输入格式
There are several test case. The first line of each test case contains an integer n, means the number of piles. Then a line with n numbers shows the number of rocks of each pile. n <= 1000. The number of rocks in each pile is smaller than 128.

输出格式
For each test case, print on line, Study brother's probability of winning.

输入样例
2
2 2
输出样例

0.500000

hint
They may get 1,1 or 1,2 or 2,1 or 2,2,so the the probability is 0.5

c++
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>

using namespace std;
int a[1005];
double dp[2][130];
int main()
{
    int n;
    while (~scanf("%d", &n))
    {
        for (int i=0; i<n; i++)
        {
            scanf("%d", &a[i]);
        }
        int pre = 0;
        int cur = 1;
        for (int i=1; i<=128; i++)
        {
            dp[pre][i] = 0;
        }
        dp[pre][0] = 1;
        for (int i=0; i<n; i++)
        {
            for (int k=0; k<=128; k++)
            {
                dp[cur][k] = 0;
                for (int j=1; j<=a[i]; j++)
                {
                    dp[cur][k] += dp[pre][j^k]/a[i];
                }
            }
            swap(cur, pre);
        }
        printf("%.6lf\n", dp[pre][0]);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值