1. 在两个数列之间

有两个整数数列 a1,a2,,anb1,b2,,bn。我们的任务是找出满足以下条件的数列 c1,c2,,cn

  • i=1,2,,naicibi
  • i=1,2,,n1cici+1
  • 所有 ci 都是整数

满足这些条件的整数数列有多少个?输出答案模 998244353 的余数。

限制:

  • 1n3000
  • 0aibi3000

分析

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 的数列 ai,求 ai 的一个长度为 m 子序列 bi,那么该子序列的贡献就是 i=1mi×bi
希望你求出这样一个子序列,使得这个子序列贡献和最大,你只需要输出这个最大值。

限制:

  • 1mn2000
  • |ai|2×105

分析

dp[i][j] 表示在序列 a 的前 i 个数中选 j 个数时 k=1jk×Bk 的最大值

代码实现
#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 的次数就是置换 σ(1,2,,n) 的逆序对
然后考虑状压dp
dp[S] 表示序列 A 的前 |S| 个元素由集合 S 中的元素构成且使得 A1=B1,,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

对于相邻两张卡片,假设卡片上的字符串分别为 ST,如果 S+T>T+S,显然交换两张卡片的顺序更好,那么我们可以按照这个规则来对所有卡片做一遍排序
然后考虑dp,记 dp[i][j] 表示在前 i 个字符串中选择 j 个字符串按次拼接起来得到的最小字符串。
但这样就有一个问题了,不同长度的字符串的大小关系不太好确定,所以需要把长度也加到dp里,这样一来状态的复杂度就是 O(NKL2),其中 L=max(Si),显然要炸
考虑从后往前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 的单个字符被压缩后的长度:

192
10993
1009994
100099995

dp[i][j] 表示前 i 个字符压缩后的长度为 j 的方案数,再令 f(x) 为长度为 x 的单个字符被压缩后的长度

转移:

可以枚举最后一段完全相同的字符的子串,得到 dp[i][j] += dp[i-w][j-f(w)] * 25

状态数为 O(N2),转移数为 O(N),时间复杂度为 O(N3)

可以用差分将转移数降到 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

给定仅由 01 构成的字符串 S。统计满足以下条件的 {1,2,,N} 的排列 P,并对答案模 998244353

  • 对于任意一对满足 Si=0Sj=1i<j(i,j),都成立 Pi<Pj

限制:

  • 1N2000

算法分析

dp[i][j] 表示已经填了前 i 个数,在剩下的数中恰有 j 个数满足它小于在前面 Si=0 的位置上所填的数的最大值
用前缀和加速可以做到 O(n2)

代码实现
#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,其计算方式如下:

  • mex 表示区间中未出现的最小非负整数
  • w=mex4

现在,你需要找到一种划分数组的方式,使得所有区间的权值之和最大,并输出这个最大权值。
注意,你无需输出具体的划分方式,只需要输出最大权值即可!

限制:

  • 2n2×105
  • 0aim2×105

算法分析

做法1:

考虑动态规划,用 dp[i] 表示将数组 [1,i] 的位置划分为若干区间所得到的最大权值总和,答案即为 dp[n]
[1,i] 的结尾处必然存在以 i 为右端点划分出来的区间,于是可以枚举这个区间的左端点 j,并计算出区间 [j,i]mex,从而得到权值 w=mex4
于是有状态转移方程:dp[i]=max(dp[i],dp[j1]+w)
枚举 i,j 的时间复杂度为 O(n2),计算区间 mex 的时间复杂度为 O(m),整体时间复杂度为 O(n2m)

做法2:

当一个区间的右端点 i 固定时,左端点 j 不断往左边移动的过程中,每移动一次,区间中就多一个整数,mex 的值就有机会在原来的基础上变大,一定不会变小,不难发现有单调性;
因此在枚举左端点 j 的过程中,可以顺便用类似双指针的方式,快速地把每次移动后区间对应的 mex 求出来,从而省去 O(m) 的复杂度,整体复杂度为 O(n2)

代码实现
#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 之前的最后一次出现的位置,这些位置就是 mex 的值可能发生变化的位置。

又因为区间权值 w=mex40,根据上文中提到的转移方程,可以得出:dp[i1]dp[i],对于所有 1in 都成立。
因此,dp[i] 的值随着 i 的增大,是单调不降的;
对于右端点 i 相同且权值 w=mex4 相同的区间 [j,i],选择 j 最大的区间去更新 dp[x] 的值即可,而 last[x] 所记录的“整数 x 在位置 i 之前的最后一次出现位置”正好就具备了 j 值最大的条件。
至此,用 O(m) 的时间复杂度从 1m 枚举 x 作为 mex 的值,从 last[x]1 的位置更新 dp[i] 即可;
而对于区间总和是否超过 m 的限制,可以预处理前缀和进行 O(1) 地判断。

注意,若出现 last[y]<last[x]y<x 的情况,则当枚举到 x 时,应当从 last[y]1 的位置更新 dp[i] 的值,因为如果 mexx,那么比它小的整数 y 必须包含在区间里面。
时间复杂度为 O(nm)

正解做法:

注意到:“mex 是区间中未出现的最小非负整数”等价于“整数 0,1,2,,mex1 均已经出现在区间中”。
再根据题目的限制条件:每个区间的元素和不能超过 m,可以得出 mex(mex1)2m,即 mex2m+1
也就是说,对于一个右端点 i 固定的区间,其左端点 j 往左移动的过程中,mex 的值至多变化 2m+1 次。
至此,可以像做法3一样,维护 last 数组,枚举 2m+1 个位置来加速每一个 dp[i] 的更新。
事实上,在代码实现的时候,当出现区间元素和大于 m 的情况时,立刻跳出 last[x] 的循环即可保证正确的时间复杂度。
时间复杂度为 O(nm)

代码实现
#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 为一个给定的整数)。

在一个段落中,若减号数量大于或等于加号,则称这个段落是负能量的;否则,就是正能量的。

请问如何划分,才能让负能量的段落达到最少,输出这个最少值。

限制:

  • 1kn3×105

算法分析

原题:重新分区