zoukankan      html  css  js  c++  java
  • 洛谷P5664 Emiya 家今天的饭 题解 动态规划

    首先来看一道题题:

    安娜写宋词

    题目背景

    洛谷P5664 Emiya 家今天的饭【民间数据】 的简化版本。

    题目描述

    安娜准备去参加宋词大赛,她一共掌握 (n)词牌名 ,并且她的宋词总共有 (m) 个不同的 主题
    为了方便描述,我们对词牌名从 (1) ~ (n) 编号,对主题从 (1) ~ (m) 编号。

    安娜准备了若干首诗,每首诗都有 恰好一个 词牌名与 恰好一个 主题。
    更具体地说,安娜为第 (i) 个词牌名第 (j) 个主题准备了 (a_{i,j}) 首宋词((1 le i le n, 1 le j le m)),这也意味着安娜总共准备了 (sum_{i=1}^n sum_{j=1}^m a_{i,j}) 首宋词。

    宋词大赛有一些规则:

    1. 每位选手至少要念一首宋词(这意味着安娜至少要选择一首诗念);
    2. 同一选手不能选择同样的两首具有相同词牌名的宋词念(这意味着同一词牌名的所有诗当中安娜最多只能选一首念);
    3. 所念的诗歌要表现主题,所以如果一位选手念了 (k) 首宋词,那么至少要有 (lfloor frac{k}2 floor + 1) 首宋词是同一主题的。

    这里的 (lfloor x floor) 为向下取整函数,表示不超过 (x) 的最大整数。

    这些要求难不倒安娜,但是她想知道共有多少种不同的符合要求的选词方案。两种方案不同,当且仅当存在至少一首宋词在一种方案中出现,而不在另一种方案中出现。

    请你帮安娜计算一下,一共有多少符合要求的选词方案。
    因为数据量可能会比较大,所以你只需要告诉她方案数对 (1,000,000,007) 取模的结果即可。

    输入格式

    输入的第 1 行包含两个用空格隔开的整数 (n, m)
    第 2 行至第 (n+1) 行,每行 (m) 个用单个空格隔开的整数,其中第 (i+1) 行的 (m) 个数依次为 (a_{i,1}, a_{i,2}, ..., a_{i,m})

    输出格式

    仅一行一个整数,表示所求方案数对 (1,000,000,007) 取模的结果。

    输入输出样例

    输入 #1

    2 3 
    1 0 1
    0 1 1
    

    输出 #1

    5
    

    输入 #2

    3 3
    1 2 3
    4 5 0
    6 0 0
    

    输出 #2

    299
    

    说明/提示

    对于100%的数据,保证 (1 le n,m le 100)(0 le a_{i,j} lt 1,000,000,007)

    问题分析

    这个问题可以归纳成:
    给你一个 (n)(m) 列的矩阵,每一行你最多可以选一个元素,并且需要保证你所选的 k 个元素有至少 (lfloor frac{k}2 floor + 1) 个出现在同一列。
    那么对于这个问题,肯定只有一列上的元素达到了总数的一半以上。
    我们不妨设这一列(选择元素数量超过一半的列)为第 (c) 列,那么在确定第 (c) 列的情况下,我们设状态 (f_{i,j,k}) 表示“前 (i) 行选择了 (j) 个元素在第 (c) 列,选择了 (k) 个元素不在第 (c) 列”的方案总数。
    则可以得到状态转移方程为:

    • (f_{0,0,0} = 1)
    • (f_{i,j,k} = f_{i-1,j,k} + f_{i-1,j-1,k} imes a_{i,j} + f_{i-1,j,k-1} imes (S_i - a_{i,j}))

    其中,(a_{i,j}) 表示第i行第j列的元素个数;(S_i) 表示 (sum_{j=1}^m s_{i,j}) ,即第 (i) 行所有元素之和。

    上述算法需要遍历 (m) 列,然后对于每一列,需要遍历 (i)(j)(k) ,所以总的时间复杂度为 (O(m imes n^3))

    优化

    其实,对于上述问题,我们并不关心 (j)(k) 的具体数值是什么,我们关心的是 (j) 是不是比 (k) 大。
    那么我们可以发现我们其实是关心的是 (j-k) 的值是不是比 (0) 大。

    然后,我们同样是枚举每一列 (c) ,然后重新定义状态 (f_{i,j}) 表示“前 (i) 行元素中选择在第 (c) 列的元素个数与不在第 (c) 列的元素个数之差为 (j) ” 的方案总数。
    则针对这个新的状态,可以得到状态转移方程为:

    • (f_{0,0} = 1)
    • (f_{i,j} = f_{i-1,j} + f_{i-1,j-1} imes a_{i,j} + f_{i-1,j+1} imes (S_i - a_{i,j}))

    其中,(a_{i,j}) 表示第i行第j列的元素个数;(S_i) 表示 (sum_{j=1}^m s_{i,j}) ,即第 (i) 行所有元素之和。

    上述算法优化了一维空间,时间复杂度变为 (O(m imes n^2))

    但是要注意的一点是,在计算 (f_{i,j}) 的时候,我们可以发现 (j) 的范围是在 ([-i, i]) 这个区间范围内的,所以我们在开数组的时候开一个 (n imes 2n) 的数组 (f[n][2n]) ,其中,状态 (f_{i,j})(f[i][j+n]) 表示。

    实现代码如下:

    #include <bits/stdc++.h>
    using namespace std;
    const long long MOD = 1000000007LL;
    const int maxn = 110;
    int n, m;
    long long a[maxn][maxn], sum[maxn], f[maxn][maxn*2], ans;
    
    void solve_col(int c) {
        f[0][n] = 1;
        for (int i = 1; i <= n; i ++)
            for (int j = n-i; j <= n+i; j ++)
                f[i][j] = (f[i-1][j] + f[i-1][j-1] * a[i][c] + f[i-1][j+1] * (sum[i] - a[i][c])) % MOD;
        for (int i = 1; i <= n; i ++) ans = (ans + f[n][i+n]) % MOD;
    }
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= m; j ++) {
                cin >> a[i][j];
                sum[i] += a[i][j];
            }
        }
        for (int i = 1; i <= m; i ++) solve_col(i);
        cout << ans << endl;
        return 0;
    }
    

    洛谷P5664 Emiya 家今天的饭

    题目链接:https://www.luogu.org/problem/P5664
    题目背景:CSP-S D2T1。

    题目描述

    Emiya 是个擅长做菜的高中生,他共掌握 (n) 种烹饪方法,且会使用 (m) 种主要食材做菜。为了方便叙述,我们对烹饪方法从 (1 sim n) 编号,对主要食材从 (1 sim m) 编号。

    Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地,Emiya 会做 (a_{i,j}) 道不同的使用烹饪方法 (i) 和主要食材 (j) 的菜((1 leq i leq n, 1 leq j leq m) ),这也意味着 Emiya 总共会做 (sumlimits_{i=1}^{n} sumlimits_{j=1}^{m} a_{i,j}) 道不同的菜。

    Emiya 今天要准备一桌饭招待 Yazid 和 Rin 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 (k) 道菜的搭配方案而言:

    • Emiya 不会让大家饿肚子,所以将做至少一道菜,即 (k geq 1)
    • Rin 希望品尝不同烹饪方法做出的菜,因此她要求每道菜的烹饪方法互不相同
    • Yazid 不希望品尝太多同一食材做出的菜,因此他要求每种主要食材至多在一半的菜(即 (lfloor frac{k}{2} floor) 道菜)中被使用

    这里的 (lfloor x floor) 为下取整函数,表示不超过 (x) 的最大整数。

    这些要求难不倒 Emiya,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。

    Emiya 找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 (998,244,353) 取模的结果。

    输入格式

    第 1 行两个用单个空格隔开的整数 (n,m)

    第 2 行至第 (n + 1) 行,每行 (m) 个用单个空格隔开的整数,其中第 (i + 1) 行的 (m) 个数依次为 (a_{i,1}, a_{i,2}, cdots, a_{i,m})

    输出格式

    仅一行一个整数,表示所求方案数对 (998,244,353) 取模的结果。

    输入输出样例

    输入 #1

    2 3 
    1 0 1
    0 1 1
    

    输出 #1

    3
    

    输入 #2

    3 3
    1 2 3
    4 5 0
    6 0 0
    

    输出 #2

    190
    

    输入 #3

    5 5
    1 0 0 1 1
    0 1 0 1 0
    1 1 1 1 0
    1 0 1 0 1
    0 1 1 0 1
    

    输出 #3

    742
    

    说明/提示

    【样例 1 解释】
    由于在这个样例中,对于每组 i, ji,j,Emiya 都最多只会做一道菜,因此我们直接通过给出烹饪方法、主要食材的编号来描述一道菜。
    符合要求的方案包括:

    • 做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 2 的菜
    • 做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 3 的菜
    • 做一道用烹饪方法 1、主要食材 3 的菜和一道用烹饪方法 2、主要食材 2 的菜

    因此输出结果为 (3 mod 998,244,353 = 3) 。 需要注意的是,所有只包含一道菜的方案都是不符合要求的,因为唯一的主要食材在超过一半的菜中出现,这不满足 Yazid 的要求。

    【样例 2 解释】
    Emiya 必须至少做 2 道菜。

    • 做 2 道菜的符合要求的方案数为 100。
    • 做 3 道菜的符合要求的方案数为 90。

    因此符合要求的方案数为 100 + 90 = 190。

    【数据范围】
    对于所有测试点,保证 (1 leq n leq 100)(1 leq m leq 2000)(0 leq a_{i,j} lt 998,244,353)

    问题分析

    题解转载自Caro23333大神的博客:https://www.luogu.org/blog/Caro23333/solution-p5664
    这个题作为d2t1比往年偏难,但完全在可以接受和预见的范围。

    首先考虑列的限制,发现若有不合法的列,则必然有且只有一列是不合法的:因为不可能有不同的两列数量都超过总数的一半。

    于是发现列的限制容易容斥计算:每行选不超过一个的方案数 - 每行选不超过一个,且某一列选了超过一半的方案数。

    那么考虑枚举不合法的一列。假设我们已经枚举了不合法的列为 (col) ,接下来会发现我们只关心一个数的位置是否在当前列;如果属于在其他列的情况,那么它具体在哪一列对当前列的合法性并无影响,我们并不需要考虑。

    接下来设计状态。
    (f_{i,j,k}) 表示对于 (col) 这一列,前 (i) 行在 (col) 列中选了 (j) 个,在其他列中选了 (k) 个,那么令 (s_i) 为第 (i) 行的总和,则有转移:

    [f_{i,j,k} = f_{i-1,j,k} + a_{i,col}* f_{i-1,j-1,k} + (s_i-a_{i,col})* f_{i-1,j,k-1} ]

    状态数 (O(n^3)) ,转移 (O(1)) ,算上枚举 (col) ,这一步复杂度是 (O(mn^3)) 的。统计如下和式的值并对每一列求和即可得到不合法的方案数:

    [sum_{j>k} f_{n,j,k} ]

    接下来考虑计算总方案数:和之前相似,只需设 (g_{i,j}) 为前 (i) 行共选了 (j) 个数的方案数,则有转移:

    [g_{i,j} = g_{i-1,j} + s_i*g_{i-1,j-1} ]

    那么 (sumlimits_{i=1}^n g_{n,i}) 就是总方案数, 这一步是 (O(n^2)) 的。所以现在可以在 (O(mn^3)) 的总复杂度内完成这题,获得84分。

    考虑进一步优化,剪去无用状态:注意到在不合法情况的计算过程中,也就是 (f_{i,j,k}) 的转移过程中,我们实际上并不关心 (j,k) 的具体数值,而只关心相对的大小关系;所以我们可以将状态变为 (f_{i,j}) ,表示前 (i) 行,当前列的数比其他列的数多了 (j) 个,则有转移:

    [f_{i,j} = f_{i-1,j} + a_{i,col}* f_{i-1,j-1} + (s_i-a_{i,col})* f_{i-1,j+1} ]

    转移仍然是 (O(1)) 的,但总复杂度降为 (O(mn^2)) ,可以通过此题。

    实现代码如下:

    #include <bits/stdc++.h>
    using namespace std;
    const long long MOD = 998244353LL;
    const int maxn = 101, maxm = 2002;
    int n, m;
    long long a[maxn][maxm], sum[maxn], ans, f[maxn][maxn<<1], g[maxn];
    void solve_tot() {
        ans = 1;
        for (int i = 1; i <= n; i ++)
            ans = ans * (sum[i] + 1) % MOD;
        ans = (ans - 1 + MOD) % MOD;
    }
    void solve_col(int col) {
        f[0][n] = 1;
        for (int i = 1; i <= n; i ++) {
            for (int j = n-i; j <= n+i; j ++) {
                f[i][j] = ( f[i-1][j] + f[i-1][j-1] * a[i][col] + f[i-1][j+1] * (sum[i] - a[i][col]) ) % MOD;
            }
        }
        long long tmp = 0;
        for (int i = 1; i <= n; i ++) tmp = (tmp + f[n][i+n]) % MOD;
        ans = (ans - tmp + MOD) % MOD;
    }
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= m; j ++) {
                cin >> a[i][j];
                sum[i] = (sum[i] + a[i][j]) % MOD;
            }
        }
        solve_tot();
        for (int i = 1; i <= m; i ++) solve_col(i);
        cout << ans << endl;
        return 0;
    }
    

    代码说明:

    • solve_tot() 函数用来计算所有方案数;
    • solve_col(int col) 函数用来计算针对第 (col) 列所有不满足要求的方案(并减去)。
  • 相关阅读:
    P2567 [SCOI2010]幸运数字 DFS+容斥定理
    Codeforces Round #462 (Div. 2) C DP
    Codeforces Round #428 (Div. 2) C. dfs
    POJ 2079 最大三角形面积(凸包)
    POJ 3608 凸包间最短距离(旋转卡壳)
    2018年全国多校算法寒假训练
    Educational Codeforces Round 37 E. Connected Components?(图论)
    UVa 1440:Inspection(带下界的最小流)***
    BZOJ 1483:[HNOI2009]梦幻布丁(链表启发式合并)
    PAT L3-016:二叉搜索树的结构(暴力)
  • 原文地址:https://www.cnblogs.com/quanjun/p/12009297.html
Copyright © 2011-2022 走看看