Atcoder-ABC159-EF 题解
ABC 159
E. Dividing Chocolate (二进制枚举,观察数据范围)
题意
有 \(H\times W\) 大小的方格矩阵,每个方格为 \(0\) or \(1\), 现在可以进行横切与竖切,询问最少切多少次可以保证最后的分块中每个分块都有不超过 \(K\) 个 \(1\)。
数据范围
\(1\leq H \leq 10\)
\(1\leq W \leq 1000\)
\(1\leq K \leq H\times W\)
思路
- 观察数据范围, \(1\leq H \leq 10\) , 所以尝试枚举横切,复杂度 \(O(2^H)\) 。
- 之后我们只关注竖切。从左往右依次遍历,贪心来切一定是最优的。
- 总复杂度 \(O(2^H*H*W)\)$
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
char S[11][1010];
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int h, w, k;
cin >> h >> w >> k;
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
cin >> S[i][j];
int ans = 1e9;
for (int s = 0; s < 1 << (h - 1); s ++) {
int cnt = __builtin_popcount(s);
int init = cnt + 1;
vector<int> v(init, 0);
bool ok = true;
for (int j = 0; j < w; j++) {
int num = 0;
vector<int> tmp(init, 0);
bool cut = false;
for (int i = 0; i < h; i++) {
if (S[i][j] == '1')
tmp[num]++;
if (tmp[num] + v[num] > k)
cut = true;
if (s >> i & 1)
num++;
}
if (cut) cnt++;
for (int i = 0; i < init; i++) {
if (cut) v[i] = tmp[i];
else v[i] += tmp[i];
if (v[i] > k) ok = false;
}
if (!ok) break;
}
if (ok) ans = min(ans, cnt);
}
cout << ans << endl;
return 0;
}
F. Knapsack for All Segments (非典型DP,DP优化)
题意
给了长度为 \(N\) 的数组 \(A\), 和一个正数 \(S\) 。
定义 \(f(L,R),1\leq L\leq R\leq N\) 为合法序列 \((x_1,x_2,...x_k)\) 的方案数:
\[L\leq x_1<x_2 <\cdots <x_k\leq R,\;A_{x_1}+A_{x_2}+\cdots+A_{x_k}=S
\]
求 \(\Sigma f(L,R) \mod 998244353\)
数据范围
\(1\leq N,S,A_i \leq 3000\)
思路
- 数据范围意味着我们可以接受 \(O(n^2)\) 的算法,并且如果 dp 的话大概率是可以把序列和 \(S\) 给放进状态里去的。
- 考虑DP的话,很容易想出时空复杂度为 \(O(n^2*S)\) 的状态, \(f[l][r][x]\) 代表区间 \([l,r]\) 和位 \([x]\) 的方案数,转移可以 \(O(1)\) 类似区间DP的想法,但显然无论时间还是空间都爆掉了,考虑优化。
- 按照题目意思,如果区间 \([l,r]\) 刚好能选出满足要求的数,对答案的贡献是 \((n-r+1)*l\), 由乘法原理得到。
- 一个小trick, 算trick嘛?应该算。考虑去掉 l,r 其中一维,这里取掉其中 1 维。同时状态值的含义也要改变。将答案状态空间按照要选取的 \(x\) 序列最右端点为不同的 \(r\) ,进行不重不漏的划分。状态值改为记录贡献相关数值的形式来计算答案。
- 状态表示: \(f[r][x]\) 表示最右端点为 \(r\) (不一定必选),左端点的下标和为 \(x\) 的方案数。普遍的来说,答案的表示则为 \(\Sigma_{i=1}^n f[i-1][s-a[i]]\) ,这样的方式能表达出每个点成为序列的最右端点的所有子集贡献。
- 状态转移:
- 枚举所有 \(0\leq k\leq s\)
- 如果 \(a[i] < k\), \(f[i][k] = f[i - 1][k - a[i]] + f[i - 1][k]\) 。
- 如果 \(a[i] == k\),\(f[i][k] = i + f[i - 1][k]\)
- 如果 \(a[i] > k\) \(f[i][k] = f[i - 1][k]\)
- 这道题答案不由状态直接表达,而是通过另一种方式间接计算,十分奇妙,值得多复习。时间复杂度为 \(O(s*n)\)
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const int N = 3010, mod = 998244353;
ll f[N][N];
void add(ll & a, ll b) {
a = (a + b) % mod;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, s;
cin >> n >> s;
vector<int> a(n + 1, 0);
for (int i = 1; i <= n; i++)
cin >> a[i];
f[0][0] = 1;
ll ans = 0;
for (int i = 1; i <= n; i++) {
if (a[i] < s) add(ans, f[i - 1][s - a[i]] * (n - i + 1));
else if (a[i] == s) add(ans, i * (n - i + 1));
for (int k = s; k; k--) {
f[i][k] = f[i - 1][k];
if (a[i] < k) {
add(f[i][k], f[i - 1][k - a[i]]);
}
else if (a[i] == k) {
add(f[i][k], i);
}
}
}
cout << ans << endl;
return 0;
}