1. 在两个数列之间
有两个整数数列 \(a_1,a_2,\cdots,a_n\) 和 \(b_1,b_2,\cdots,b_n\)。我们的任务是找出满足以下条件的数列 \(c_1,c_2,\cdots,c_n\):
- 对 \(i=1,2,\cdots,n\),\(a_i \le c_i \le b_i\)
- 对 \(i=1,2,\cdots,n-1\),\(c_i \le c_{i+1}\)
- 所有 \(c_i\) 都是整数
满足这些条件的整数数列有多少个?输出答案模 \(998244353\) 的余数。
限制:
- \(1 \le n \le 3000\)
- \(0 \le a_i \le b_i \le 3000\)
分析
记 dp[i][j]
表示长度为 \(i\) 且结尾的值为 \(j\) 的合法 \(c\) 的个数
状态转移需要用前缀和优化
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
const int mod = 998244353;
//const int mod = 1000000007;
struct mint {
ll x;
mint(ll x=0):x((x%mod+mod)%mod) {}
mint operator-() const {
return mint(-x);
}
mint& operator+=(const mint a) {
if ((x += a.x) >= mod) x -= mod;
return *this;
}
mint& operator-=(const mint a) {
if ((x += mod-a.x) >= mod) x -= mod;
return *this;
}
mint& operator*=(const mint a) {
(x *= a.x) %= mod;
return *this;
}
mint operator+(const mint a) const {
return mint(*this) += a;
}
mint operator-(const mint a) const {
return mint(*this) -= a;
}
mint operator*(const mint a) const {
return mint(*this) *= a;
}
mint pow(ll t) const {
if (!t) return 1;
mint a = pow(t>>1);
a *= a;
if (t&1) a *= *this;
return a;
}
// for prime mod
mint inv() const {
return pow(mod-2);
}
mint& operator/=(const mint a) {
return *this *= a.inv();
}
mint operator/(const mint a) const {
return mint(*this) /= a;
}
};
istream& operator>>(istream& is, mint& a) {
return is >> a.x;
}
ostream& operator<<(ostream& os, const mint& a) {
return os << a.x;
}
int main() {
int n;
cin >> n;
vector<int> a(n), b(n);
rep(i, n) cin >> a[i];
rep(i, n) cin >> b[i];
const int M = 3001;
vector<mint> dp(M+1);
dp[0] = 1;
rep(i, n) {
vector<mint> p(M+1);
swap(dp, p);
rep(j, M) {
if (a[i] <= j and j <= b[i]) {
dp[j] += p[j];
}
if (j < M) p[j+1] += p[j];
}
}
mint ans;
rep(i, M) ans += dp[i];
cout << ans << '\n';
return 0;
}
2. 子序列的贡献
给定一个长度为 \(n\) 的数列 \(a_i\),求 \(a_i\) 的一个长度为 \(m\) 子序列 \(b_i\),那么该子序列的贡献就是 \(\sum\limits_{i=1}^m i \times b_i\)
希望你求出这样一个子序列,使得这个子序列贡献和最大,你只需要输出这个最大值。
限制:
- \(1 \leqslant m \leqslant n \leqslant 2000\)
- \(|a_i| \leqslant 2 \times 10^5\)
分析
记 dp[i][j]
表示在序列 \(a\) 的前 \(i\) 个数中选 \(j\) 个数时 \(\sum\limits_{k=1}^j k \times B_k\) 的最大值
代码实现
#include <bits/extc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
inline void chmax(ll& x, ll y) { if (x < y) x = y; }
int main() {
int n, m;
cin >> n >> m;
vector<ll> a(n);
rep(i, n) cin >> a[i];
const ll INF = 1e18;
vector dp(n+1, vector<ll>(m+1, -INF));
dp[0][0] = 0;
rep(i, n) {
rep(j, m+1) {
chmax(dp[i+1][j], dp[i][j]);
if (j) chmax(dp[i+1][j], dp[i][j-1]+a[i]*j);
}
}
cout << dp[n][m] << '\n';
return 0;
}
3. ABC232F
注意到操作2的本质其实是遍历序列 \(A\) 的所有可能的顺序有 \(n!\) 种,记为 \(A'\),那么从 \(A\) 变到 \(A'\) 的次数就是置换 \(\sigma(1, 2, \dots, n)\) 的逆序对
然后考虑状压dp
记 dp[S]
表示序列 \(A'\) 的前 \(|S|\) 个元素由集合 \(S\) 中的元素构成且使得 \(A_1' = B_1,
\cdots, A_{|S|}' = B_{|S|}\) 的最小费用
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
inline void chmin(ll& x, ll y) { if (x > y) x = y; }
int main() {
int n;
ll x, y;
cin >> n >> x >> y;
vector<int> a(n), b(n);
rep(i, n) cin >> a[i];
rep(i, n) cin >> b[i];
int n2 = 1<<n;
const ll INF = 1e18;
vector<ll> dp(n2, INF);
dp[0] = 0;
rep(s, n2) {
int j = __builtin_popcount(s);
rep(i, n) {
if (s>>i&1) continue;
ll cost = abs(a[i]-b[j])*x;
cost += __builtin_popcount(s>>i)*y;
chmin(dp[s|1<<i], dp[s]+cost);
}
}
ll ans = dp[n2-1];
cout << ans << '\n';
return 0;
}
4. ABC225F
对于相邻两张卡片,假设卡片上的字符串分别为 \(S\) 和 \(T\),如果 \(S+T > T+S\),显然交换两张卡片的顺序更好,那么我们可以按照这个规则来对所有卡片做一遍排序
然后考虑dp,记 dp[i][j]
表示在前 \(i\) 个字符串中选择 \(j\) 个字符串按次拼接起来得到的最小字符串。
但这样就有一个问题了,不同长度的字符串的大小关系不太好确定,所以需要把长度也加到dp里,这样一来状态的复杂度就是 \(O(NKL^2)\),其中 \(L = \max(S_i)\),显然要炸
考虑从后往前dp,也就是先确定好后缀,这样就不会有问题了
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
inline void chmin(string& a, string b) {
if (b == "") return;
if (a == "" or a > b) a = b;
}
int main() {
int n, k;
cin >> n >> k;
vector<string> s(n);
rep(i, n) cin >> s[i];
sort(s.rbegin(), s.rend(), [&](string a, string b) {
return a+b < b+a;
});
vector<string> dp(k+1);
for (string a : s) {
for (int j = k-1; j >= 0; --j) {
if (dp[j] == "" and j) continue;
chmin(dp[j+1], a+dp[j]);
}
}
cout << dp[k] << '\n';
return 0;
}
5. ABC249E
考察长度为 \(x\) 的单个字符被压缩后的长度:
\(1 \sim 9 \to 2\)
\(10 \sim 99 \to 3\)
\(100 \sim 999 \to 4\)
\(1000 \sim 9999 \to 5\)
记 dp[i][j]
表示前 \(i\) 个字符压缩后的长度为 \(j\) 的方案数,再令 \(f(x)\) 为长度为 \(x\) 的单个字符被压缩后的长度
转移:
可以枚举最后一段完全相同的字符的子串,得到 dp[i][j] += dp[i-w][j-f(w)] * 25
状态数为 \(\mathcal{O}(N^2)\),转移数为 \(\mathcal{O}(N)\),时间复杂度为 \(\mathcal{O}(N^3)\)
可以用差分将转移数降到 \(\mathcal{O}(1)\)
代码实现
#include <bits/stdc++.h>
#if __has_include(<atcoder/all>)
#include <atcoder/all>
using namespace atcoder;
#endif
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using mint = modint;
const int ten[] = {0, 1, 10, 100, 1000, 10000};
int main() {
int n, p;
cin >> n >> p;
mint::set_mod(p);
vector dp(n+1, vector<mint>(n));
vector ds(n+1, vector<mint>(n));
auto f = [&](int x) {
int res = 0;
while (x) {
++res;
x /= 10;
}
return res+1;
};
for (int w = 1; w <= n; ++w) {
dp[w][f(w)] = 26;
}
for (int i = 1; i <= n; ++i) {
rep(j, n) {
// for (int w = 1; w <= i; ++w) {
// int nj = j-f(w);
// if (nj < 0) continue;
// dp[i][j] += dp[i-w][nj]*25;
// }
for (int k = 1; k <= 4; ++k) {
int nj = j-k-1;
if (nj < 0) continue;
int lb = ten[k], ub = ten[k+1];
ub = min(ub, i);
if (lb >= ub) continue;
//for (int w = lb; w < ub; ++w) {
// dp[i][j] += dp[i-w][nj]*25;
//} // (i-ub+1) ... (i-lb)
mint sum = ds[i-lb][nj] - ds[i-ub][nj];
dp[i][j] += sum*25;
}
ds[i][j] = ds[i-1][j] + dp[i][j];
}
}
mint ans;
rep(j, n) ans += dp[n][j];
cout << ans.val() << '\n';
return 0;
}
6. Taffy Permutation
给定仅由 \(0\) 和 \(1\) 构成的字符串 \(S\)。统计满足以下条件的 \(\{1, 2, \cdots, N\}\) 的排列 \(P\),并对答案模 \(998244353\):
- 对于任意一对满足 \(S_i = 0\) 且 \(S_j = 1\) 且 \(i < j\) 的 \((i, j)\),都成立 \(P_i < P_j\)
限制:
- \(1 \leqslant N \leqslant 2000\)
算法分析
记 dp[i][j]
表示已经填了前 \(i\) 个数,在剩下的数中恰有 \(j\) 个数满足它小于在前面 \(S_i = 0\) 的位置上所填的数的最大值
用前缀和加速可以做到 \(\mathcal{O}(n^2)\)
代码实现
#include <bits/stdc++.h>
#if __has_include(<atcoder/all>)
#include <atcoder/all>
using namespace atcoder;
#endif
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using mint = modint998244353;
int main() {
int n;
string s;
cin >> n >> s;
vector<mint> dp(n+1);
dp[0] = 1;
rep(i, n) {
int m = n-i;
vector<mint> old(m);
swap(dp, old);
if (s[i] == '1') {
rep(j, m) dp[j] = old[j]*(n-i-j);
}
else {
rep(j, m) dp[j] = old[j+1]*(j+1);
mint sum;
rep(j, m) {
sum += old[j];
dp[j] += sum;
}
}
}
mint ans = dp[0];
cout << ans.val() << '\n';
return 0;
}
7. 数组划分
给定一个长度为 \(n\) 的数组 \(a\),你需要将数组划分为若干个互不相交的连续区间,使得每个区间中的元素总和不超过 \(m\),并且数组的每个元素都恰好属于其中一个区间。
定义每个区间的权值为 \(w\),其计算方式如下:
- 令 \(\operatorname{mex}\) 表示区间中未出现的最小非负整数
- 则 \(w = \operatorname{mex}^4\)
现在,你需要找到一种划分数组的方式,使得所有区间的权值之和最大,并输出这个最大权值。
注意,你无需输出具体的划分方式,只需要输出最大权值即可!
限制:
- \(2 \leqslant n \leqslant 2 \times 10^5\)
- \(0 \leqslant a_i \leqslant m \leqslant 2 \times 10^5\)
算法分析
做法1:
考虑动态规划,用 dp[i]
表示将数组 \([1, i]\) 的位置划分为若干区间所得到的最大权值总和,答案即为 \(dp[n]\)
在 \([1, i]\) 的结尾处必然存在以 \(i\) 为右端点划分出来的区间,于是可以枚举这个区间的左端点 \(j\),并计算出区间 \([j, i]\) 的 \(\operatorname{mex}\),从而得到权值 \(w = \operatorname{mex}^4\)
于是有状态转移方程:\(dp[i] = \max(dp[i], dp[j-1]+w)\)
枚举 \(i, j\) 的时间复杂度为 \(O(n^2)\),计算区间 \(\operatorname{mex}\) 的时间复杂度为 \(O(m)\),整体时间复杂度为 \(O(n^2m)\)。
做法2:
当一个区间的右端点 \(i\) 固定时,左端点 \(j\) 不断往左边移动的过程中,每移动一次,区间中就多一个整数,\(\operatorname{mex}\) 的值就有机会在原来的基础上变大,一定不会变小,不难发现有单调性;
因此在枚举左端点 \(j\) 的过程中,可以顺便用类似双指针的方式,快速地把每次移动后区间对应的 \(\operatorname{mex}\) 求出来,从而省去 \(O(m)\) 的复杂度,整体复杂度为 \(O(n^2)\)。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
using ll = long long;
int a[200005];
ll dp[200005];
bool used[200005];
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, m;
cin >> n >> m;
rep(i, n) cin >> a[i];
rep(i, n) {
memset(used, 0, sizeof used);
int mex = 0; ll sum = 0;
for (int j = i; j >= 1; --j) {
sum += a[j];
if (sum > m) continue;
used[a[j]] = true;
while (used[mex]) ++mex;
ll w = (ll)mex*mex*mex*mex;
dp[i] = max(dp[i], dp[j-1]+w);
}
}
cout << dp[n] << '\n';
return 0;
}
做法3:
当 \(i\) 从小到大遍历去求 \(dp[i]\) 的值时,用数组 last[x]
去记录整数 \(x\) 在位置 \(i\) 之前的最后一次出现的位置,这些位置就是 \(\operatorname{mex}\) 的值可能发生变化的位置。
又因为区间权值 \(w = \operatorname{mex}^4 \geqslant 0\),根据上文中提到的转移方程,可以得出:\(dp[i-1] \leqslant dp[i]\),对于所有 \(1 \leqslant i \leqslant n\) 都成立。
因此,\(dp[i]\) 的值随着 \(i\) 的增大,是单调不降的;
对于右端点 \(i\) 相同且权值 \(w=\operatorname{mex}^4\) 相同的区间 \([j, i]\),选择 \(j\) 最大的区间去更新 \(dp[x]\) 的值即可,而 \(last[x]\) 所记录的“整数 \(x\) 在位置 \(i\) 之前的最后一次出现位置”正好就具备了 \(j\) 值最大的条件。
至此,用 \(O(m)\) 的时间复杂度从 \(1\) 到 \(m\) 枚举 \(x\) 作为 \(\operatorname{mex}\) 的值,从 \(last[x]-1\) 的位置更新 \(dp[i]\) 即可;
而对于区间总和是否超过 \(m\) 的限制,可以预处理前缀和进行 \(O(1)\) 地判断。
注意,若出现 \(last[y] < last[x]\) 且 \(y < x\) 的情况,则当枚举到 \(x\) 时,应当从 \(last[y]-1\) 的位置更新 \(dp[i]\) 的值,因为如果 \(\operatorname{mex}\) 为 \(x\),那么比它小的整数 \(y\) 必须包含在区间里面。
时间复杂度为 \(O(nm)\)。
正解做法:
注意到:“\(\operatorname{mex}\) 是区间中未出现的最小非负整数”等价于“整数 \(0, 1, 2, \cdots, \operatorname{mex}-1\) 均已经出现在区间中”。
再根据题目的限制条件:每个区间的元素和不能超过 \(m\),可以得出 \(\frac{\operatorname{mex}(\operatorname{mex}-1)}{2} \leqslant m\),即 \(\operatorname{mex} \leqslant \sqrt{2m}+1\)。
也就是说,对于一个右端点 \(i\) 固定的区间,其左端点 \(j\) 往左移动的过程中,\(\operatorname{mex}\) 的值至多变化 \(\sqrt{2m}+1\) 次。
至此,可以像做法3一样,维护 \(last\) 数组,枚举 \(\sqrt{2m}+1\) 个位置来加速每一个 \(dp[i]\) 的更新。
事实上,在代码实现的时候,当出现区间元素和大于 \(m\) 的情况时,立刻跳出 \(last[x]\) 的循环即可保证正确的时间复杂度。
时间复杂度为 \(O(n\sqrt{m})\)
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
using ll = long long;
int a[200005];
ll s[200005], dp[200005];
int last[200005];
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, m;
cin >> n >> m;
rep(i, n) cin >> a[i];
rep(i, n) s[i] = s[i-1]+a[i];
rep(i, n) {
last[a[i]] = i;
dp[i] = dp[i-1];
int best = n;
for (int x = 0; x <= m; ++x) {
if (last[x] == 0) break;
if (s[i]-s[last[x]-1] > m) break;
best = min(best, last[x]);
ll w = ll(x+1)*(x+1)*(x+1)*(x+1);
dp[i] = max(dp[i], dp[best-1]+w);
}
}
cout << dp[n] << '\n';
return 0;
}
8. 区间划分
给定 \(n\) 个连成一串的符号。符号只可能是 +
或者是 -
。我们需要将这些符号划分成几个区间段落。每一段至少一个符号,至多 \(k\) 个符号(\(k\) 为一个给定的整数)。
在一个段落中,若减号数量大于或等于加号,则称这个段落是负能量的;否则,就是正能量的。
请问如何划分,才能让负能量的段落达到最少,输出这个最少值。
限制:
- \(1 \leqslant k \leqslant n \leqslant 3 \times 10^5\)
算法分析
原题:重新分区