其他分享
首页 > 其他分享> > 洛谷P5664 Emiya 家今天的饭 题解 动态规划

洛谷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 \rfloor + 1\) 首宋词是同一主题的。

这里的 \(\lfloor x \rfloor\) 为向下取整函数,表示不超过 \(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 \rfloor + 1\) 个出现在同一列。
那么对于这个问题,肯定只有一列上的元素达到了总数的一半以上。
我们不妨设这一列(选择元素数量超过一半的列)为第 \(c\) 列,那么在确定第 \(c\) 列的情况下,我们设状态 \(f_{i,j,k}\) 表示“前 \(i\) 行选择了 \(j\) 个元素在第 \(c\) 列,选择了 \(k\) 个元素不在第 \(c\) 列”的方案总数。
则可以得到状态转移方程为:

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

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

优化

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

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

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

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

但是要注意的一点是,在计算 \(f_{i,j}\) 的时候,我们可以发现 \(j\) 的范围是在 \([-i, i]\) 这个区间范围内的,所以我们在开数组的时候开一个 \(n \times 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 总共会做 \(\sum\limits_{i=1}^{n} \sum\limits_{j=1}^{m} a_{i,j}\) 道不同的菜。

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

这里的 \(\lfloor x \rfloor\) 为下取整函数,表示不超过 \(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 都最多只会做一道菜,因此我们直接通过给出烹饪方法、主要食材的编号来描述一道菜。
符合要求的方案包括:

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

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

因此符合要求的方案数为 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}\]

那么 \(\sum\limits_{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;
}

代码说明:

标签:方案,洛谷,P5664,题解,sum,食材,maxn,Emiya,col
来源: https://www.cnblogs.com/quanjun/p/12009297.html