标签: 动态规划

[JLOI2013]卡牌游戏 题解

[JLOI2013]卡牌游戏 题解

题目地址:洛谷:【P2059】[JLOI2013]卡牌游戏 – 洛谷、BZOJ 

[SCOI2010]生成字符串 题解

[SCOI2010]生成字符串 题解

题目地址:洛谷:【P1641】[SCOI2010]生成字符串 – 洛谷、BZO 

[洛谷1357]花园 题解

[洛谷1357]花园 题解

题目地址:洛谷:【P1357】花园 – 洛谷

题目描述

小L有一座环形花园,沿花园的顺时针方向,他把各个花圃编号为1~N(2<=N<=10^15)。他的环形花园每天都会换一个新花样,但他的花园都不外乎一个规则,任意相邻M(2<=M<=5,M<=N)个花圃中有不超过K(1<=K<M)个C形的花圃,其余花圃均为P形的花圃。
例如,N=10,M=5,K=3。则
CCPCPPPPCC 是一种不符合规则的花圃;
CCPPPPCPCP 是一种符合规则的花圃。
请帮小L求出符合规则的花园种数Mod 1000000007

由于请您编写一个程序解决此题。

输入输出格式

输入格式:
一行,三个数N,M,K。

输出格式:
花园种数Mod 1000000007

输入输出样例

输入样例#1:

10 5 3

输出样例#1:

458

输入样例#2:

6 2 1

输出样例#2:

18

说明

【数据规模】
40%的数据中,N<=20;
60%的数据中,M=2;
80%的数据中,N<=10^5。
100%的数据中,N<=10^15。

题解

我们看一下M=2怎么做。我们枚举后面2个花圃的状态,一共有4种:CC、CP、PC、PP,设计状态dp[i][S]表示到第i个且末尾2个花圃的状态为S的方案数,然后枚举后面接上去哪个花圃来转移,如CC可以转移到CC和CP。如果M更大,到5了,就可以考虑状压这个状态来转移。转移同上,只是要把操作换为位运算。这样直接做的复杂度是[eq]O(n \cdot 2^{m})[/eq]的。
当n变得无法承受的时候,我们需要一个[eq]O(\log n)[/eq]的算法。这个时候我们想到了快速幂。我们知道,从一个状态向下一个状态的转移是一个固定不变的线性变换,既然如此,我们构建转移矩阵,进行矩阵快速幂计算转移矩阵的n次方即可。复杂度是[eq]O(\log n)[/eq]的(忽略矩阵乘法带来的开销)。
对于构造转移矩阵的方法,实际上矩阵的第i行表示下一层的i这个元素由这一层的元素乘上怎么样的系数加和得来的,因此对于i \rightarrow i'这个转移,我们直接令trans_{i', i} = 1即可。由于初始的时候是一个单位矩阵,也可以不乘这个初始矩阵。
为什么不是n-m次幂?注意我们DP状态的定义,答案在dp[n][S]中,至于当i小于m的时候,实际上S多出来那部分的意义是在枚举这个序列末尾的情况,因为花园是个环嘛。答案计算的是[eq]\sum_S dp[n][S] = trans^n_{S, S}[/eq],是因为初始的时候是个列向量,是一排1,扩展成方阵以后就是单位矩阵了,最后肯定要按照列向量的意义计算,就取主对角线上的数值。

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    register char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 50, MO = 1e9 + 7;

LL n, m, k, cnt[MAXN];

struct Matrix {
    LL a[MAXN][MAXN];
    Matrix() {
        memset(a, 0, sizeof(a));
    }
    inline Matrix operator*(const Matrix &rhs) const {
        Matrix res;
        for(int i = 0; i < 1 << m; i++) {
            for(int j = 0; j < 1 << m; j++) {
                for(int k = 0; k < 1 << m; k++) {
                    res.a[i][j] = (res.a[i][j] + a[i][k] * rhs.a[k][j] % MO) % MO;
                }
            }
        }
        return res;
    }
    inline Matrix& operator*=(const Matrix &x) {
        return *this = *this * x;
    }
};

inline Matrix fpow(Matrix mat, LL k) {
    Matrix res;
    for(int i = 0; i < 1 << m; i++) {
        res.a[i][i] = 1;
    }
    while(k) {
        if(k & 1) res *= mat;
        mat *= mat;
        k >>= 1;
    }
    return res;
}

int main() {
    n = readint(); m = readint(); k = readint();
    for(int i = 0; i < 1 << m; i++) {
        cnt[i] = cnt[i >> 1] + (i & 1);
    }
    Matrix trans;
    for(int i = 0; i < 1 << m; i++) {
        if(cnt[i] > k) continue;
        trans.a[i >> 1][i] = 1;
        trans.a[i >> 1 | (1 << (m - 1))][i] = 1;
    }
    trans = fpow(trans, n);
    LL res = 0;
    for(int i = 0; i < 1 << m; i++) {
        res = (res + trans.a[i][i]) % MO;
    }
    printf("%lld", res);
    return 0;
}
[SDOI2010]地精部落 题解

[SDOI2010]地精部落 题解

题目地址:洛谷:【P2467】[SDOI2010]地精部落 – 洛谷、BZOJ 

[SCOI2005]最大子矩阵 题解

[SCOI2005]最大子矩阵 题解

题目地址:洛谷:【P2331】[SCOI2005]最大子矩阵 – 洛谷、BZO 

[SDOI2009]学校食堂 题解

[SDOI2009]学校食堂 题解

题目地址:洛谷:【P2157】[SDOI2009]学校食堂 – 洛谷、BZOJ:Problem 1226. — [SDOI2009]学校食堂Dining

题目描述

小F 的学校在城市的一个偏僻角落,所有学生都只好在学校吃饭。学校有一个食堂,虽然简陋,但食堂大厨总能做出让同学们满意的菜肴。当然,不同的人口味也不一定相同,但每个人的口味都可以用一个非负整数表示。 由于人手不够,食堂每次只能为一个人做菜。做每道菜所需的时间是和前一道菜有关的,若前一道菜的对应的口味是a,这一道为b,则做这道菜所需的时间为(a or b)-(a and b),而做第一道菜是不需要计算时间的。其中,or 和and 表示整数逐位或运算及逐位与运算,C语言中对应的运算符为“|”和“&”。
学生数目相对于这个学校还是比较多的,吃饭做菜往往就会花去不少时间。因此,学校食堂偶尔会不按照大家的排队顺序做菜,以缩短总的进餐时间。
虽然同学们能够理解学校食堂的这种做法,不过每个同学还是有一定容忍度的。也就是说,队伍中的第i 个同学,最多允许紧跟他身后的Bi 个人先拿到饭菜。一旦在此之后的任意同学比当前同学先拿到饭,当前同学将会十分愤怒。因此,食堂做菜还得照顾到同学们的情绪。 现在,小F 想知道在满足所有人的容忍度这一前提下,自己的学校食堂做完这些菜最少需要多少时间。

输入输出格式

输入格式:
第一行包含一个正整数C,表示测试点的数据组数。 每组数据的第一行包含一个正整数N,表示同学数。 每组数据的第二行起共N行,每行包含两个用空格分隔的非负整数Ti和Bi,表示按队伍顺序从前往后的每个同学所需的菜的口味和这个同学的忍受度。 每组数据之间没有多余空行。

输出格式:
包含C行,每行一个整数,表示对应数据中食堂完成所有菜所需的最少时间。

输入输出样例

输入样例#1:

2
5
5 2
4 1
12 0
3 3
2 2
2
5 0
4 0

输出样例#1:

16
1

说明

对于第一组数据:
同学1允许同学2或同学3在他之前拿到菜;同学2允许同学3在他之前拿到菜;同学3比较小气,他必须比他后面的同学先拿菜……
一种最优的方案是按同学3、同学2、同学1、同学4、同学5做菜,每道菜所需的时间分别是0、8、1、6及1。
【数据规模和约定】
对于30%的数据,满足1 ≤ N ≤ 20。
对于100%的数据,满足1 ≤ N ≤ 1,000,0 ≤ Ti ≤ 1,000,0 ≤ Bi ≤ 7,1 ≤ C ≤ 5。
存在30%的数据,满足0 ≤ Bi ≤ 1。
存在65%的数据,满足0 ≤ Bi ≤ 5。
存在45%的数据,满足0 ≤ Ti ≤ 130。

题解

学会观察数据范围是一个非常有用的技巧。
我们需要一个规模大致是O(n^2)的算法。对于这题,我们实际上要找的是一个排列,使得满足每个人的限制条件且两两异或的和最小。由于限制条件的存在,我们似乎无法贪心处理,得使用DP。那DP把啥放状态里呢,分层处理n个同学是没问题的,但是怎么来处理之后的同学排到前面去这个问题呢?我们注意到Bi的范围是很小的,就可以状压了。然后再记一下最后做的是谁的菜,便于计算下一道菜的时间,这样状态就完成了。dp[i][S][j]表示前i-1个同学的菜做完了,i及i后面7位同学的情况是S,且最后一位做菜的是i+j这个状态。需要注意的是,由于j可能最小取到-8,因此需要对j+8处理成非负整数。
接着我们考虑转移问题。有个显然的情况就是当i的菜已经做了,那么我们可以把状态dp[i][S][j]推到dp[i+1][S\i][j-1]去,因为它们是等价的状态。而另外一种状态需要枚举后面这8个同学中下一个做谁的菜比较优,注意此处枚举的时候要考虑前面同学的限制,可以记录前面同学最近允许后面哪个同学比自己前,超过限制就停止枚举。我们对于每一层先枚举后8个同学的状态,再枚举上一个做菜的同学,实现层间转移和向下一层的转移。由于本题的转移情况比较复杂,这里并不能写出转移方程。
本题在状压上下了很大的功夫,只要想到状压存状态,其他的就可以暴力枚举解决。状压真是一个暴力的东西啊。

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 1005, INF = 0x3f3f3f3f;

int T, n, t[MAXN], b[MAXN], dp[MAXN][1 << 8][20];

int main() {
    T = readint();
    while(T--) {
        n = readint();
        for(int i = 1; i <= n; i++) {
            t[i] = readint(); b[i] = readint();
        }
        memset(dp, 0x3f, sizeof(dp));
        dp[1][0][7] = 0;
        for(int i = 1; i <= n; i++) {
            for(int j = 0; j < (1 << 8); j++) {
                for(int k = -8; k <= 7; k++) {
                    if(dp[i][j][k + 8] == INF) continue;
                    if(j & 1) {
                        dp[i + 1][j >> 1][k + 7] = std::min(dp[i + 1][j >> 1][k + 7], 
                            dp[i][j][k + 8]);
                    } else {
                        int lim = INF;
                        for(int p = 0; p <= 7; p++) {
                            if(j & (1 << p)) continue;
                            if(i + p > lim) break;
                            lim = std::min(lim, i + p + b[i + p]);
                            dp[i][j | (1 << p)][p + 8] = std::min(dp[i][j | (1 << p)][p + 8], 
                                dp[i][j][k + 8] + (i + k ? t[i + k] ^ t[i + p] : 0));
                        }
                    }
                }
            }
        }
        int ans = INF;
        for(int i = 0; i <= 8; i++) {
            ans = std::min(ans, dp[n + 1][0][i]);
        }
        printf("%d\n", ans);
    }
    return 0;
}
[JLOI2016]成绩比较 题解

[JLOI2016]成绩比较 题解

题目地址:洛谷:【P3270】[JLOI2016]成绩比较 – 洛谷、BZOJ 

[SHOI2012]随机树 题解

[SHOI2012]随机树 题解

题目地址:洛谷:【P3830】[SHOI2012]随机树 – 洛谷 题目描述  

[SCOI2008]奖励关 题解

[SCOI2008]奖励关 题解

题目地址:洛谷:【P2473】[SCOI2008]奖励关 – 洛谷、BZOJ:Problem 1076. — [SCOI2008]奖励关

题目描述

你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关。在这个奖励关里,系统将依次随机抛出k次宝物,每次你都可以选择吃或者不吃(必须在抛出下一个宝物之前做出选择,且现在决定不吃的宝物以后也不能再吃)。
宝物一共有n种,系统每次抛出这n种宝物的概率都相同且相互独立。也就是说,即使前k-1 次系统都抛出宝物1(这种情况是有可能出现的,尽管概率非常小),第k次抛出各个宝物的概率依然均为1/n。
获取第 i 种宝物将得到Pi分,但并不是每种宝物都是可以随意获取的。第i种宝物有一个前提宝物集合Si。只有当Si中所有宝物都至少吃过一次,才能吃第i 种宝物(如果系统抛出了一个目前不能吃的宝物,相当于白白的损失了一次机会)。注意,Pi 可以是负数,但如果它是很多高分宝物的前提,损失短期利益而吃掉这个负分宝物将获得更大的长期利益。
假设你采取最优策略,平均情况你一共能在奖励关得到多少分值?

输入输出格式

输入格式:
第一行为两个正整数k 和n,即宝物的数量和种类。以下n行分别描述一种宝物,其中第一个整数代表分值,随后的整数依次代表该宝物的各个前提宝物(各宝物编号为1到n),以0结尾。

输出格式:
输出一个实数,保留六位小数,即在最优策略下平均情况的得分。

输入输出样例

输入样例#1:

1 2
1 0
2 0

输出样例#1:

1.500000

输入样例#2:

6 6
12 2 3 4 5 0
15 5 0
-2 2 4 5 0
-11 2 5 0
5 0
1 2 4 5 0

输出样例#2:

10.023470

说明

1 <= k <= 100, 1 <= n <= 15,分值为[-106,106]内的整数。

题解

首先看数据范围像状压。我们用dp[i][S]表示到第i次奖励且获得奖励的状态为S的期望值。考虑向后转移,枚举下一关获得了什么奖励,然后判断该奖励可以不可以用,但是S这个状态是否合法(能否到达这一状态,S中1的数量是否超过了i)是一件很麻烦的事情,而且答案还得枚举S在dp[k][S]中找,不如倒推。
倒推则考虑已经有一个S,枚举这一次获得什么奖励,然后判断是否能获得,从下一次转移而来,这样一是合法状态向合法状态转移没有检验环节,二是答案就是dp[1][0]也不用枚举S,就比正推方便很多。

代码

// Code by KSkun, 2018/4
#include <cstdio>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 17, MAXK = 105;

int k, n, t, dep[MAXN], w[MAXN];
double dp[MAXK][1 << MAXN];

int main() {
    k = readint(); n = readint();
    for(int i = 1; i <= n; i++) {
        w[i] = readint();
        while(t = readint()) {
            dep[i] |= 1 << (t - 1);
        }
    }
    for(int i = k; i >= 1; i--) {
        for(int j = 0; j < 1 << n; j++) {
            for(int k = 1; k <= n; k++) {
                if((j | dep[k]) == j) {
                    dp[i][j] += std::max(dp[i + 1][j], dp[i + 1][j | (1 << (k - 1))] + w[k]);
                } else {
                    dp[i][j] += dp[i + 1][j];
                }
            }
            dp[i][j] /= n;
        }
    }
    printf("%.6lf", dp[1][0]);
    return 0;
}
[SDOI2015]序列统计 题解

[SDOI2015]序列统计 题解

题目地址:洛谷:【P3321】[SDOI2015]序列统计 – 洛谷、BZOJ