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\)

算法分析

原题:重新分区