比赛链接:
https://ac.nowcoder.com/acm/contest/33194
A.Car Show
题意:
长度为 \(n\) 的一个序列 \(a\),问有多少个区间中 \([1, m]\) 的数都出现过。
思路:
双指针。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL n, m;
cin >> n >> m;
vector <LL> a(n + 1);
for (int i = 1; i <= n; i ++ )
cin >> a[i];
vector <LL> cnt(m + 1);
LL ans = 0, sum = 0;
for (int i = 1, j = 0; i <= n; i ++ ){
while(sum < m && j < n){
j ++ ;
cnt[a[j]] ++ ;
if (cnt[a[j]] == 1){
sum ++ ;
}
}
if (sum == m) ans += n - j + 1;
cnt[a[i]] -- ;
if (cnt[a[i]] == 0) sum -- ;
}
cout << ans << "\n";
return 0;
}
B.Two Frogs
题意:
有 \(n\) 片荷叶,刚开始两只青蛙在第 1 片荷叶上,在第 \(i\) 片荷叶上会跳到 \([i + 1, i + a[i]]\) 这个区间中的任意一片荷叶上,问两只青蛙花同样的步数跳到第 \(n\) 片荷叶上的概率。
思路:
定义 \(dp[i][j]\) 表示跳到第 \(i\) 片荷叶上花了 \(j\) 步的概率。
每次 \(dp[i][j]\) 可以转移到 \(dp[i + 1][j], dp[i + 2][j], ... dp[i + a[i][j]\) 上,是 \(O(n^3)\) 的,可以发现如果 \(i\) 和 \(j\) 反一下的话,可以通过前缀和/差分优化掉一个 \(n\)。
于是定义 \(dp[j][i]\) 表示花了 \(j\) 步跳到第 \(i\) 片荷叶上的概率,再通过前缀和/差分去实现转移。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int P = 998244353;
LL qp(LL a, LL k, LL p){
LL ans = 1;
while (k){
if (k & 1) ans = ans * a % p;
k >>= 1;
a = a * a % p;
}
return ans;
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL n;
cin >> n;
vector <LL> inv(n + 1);
inv[1] = 1;
for (int i = 1; i <= n; i ++ )
inv[i] = qp(i, P - 2, P);
vector <LL> a(n);
for (int i = 1; i < n; i ++ )
cin >> a[i];
vector dp(n + 1, vector<LL>(n + 2, 0));
dp[0][1] = 1;
for (int j = 0; j < n; j ++ ){
if (j){
for (int i = 1; i <= n; i ++ )
dp[j][i] = (dp[j][i] + dp[j][i - 1]) % P;
}
for (int i = 1; i < n; i ++ ){
dp[j + 1][i + 1] = (dp[j + 1][i + 1] + inv[a[i]] * dp[j][i] % P) % P;
dp[j + 1][i + a[i] + 1] = (dp[j + 1][i + a[i] + 1] - inv[a[i]] * dp[j][i] % P) % P;
}
}
LL ans = 0;
for (int j = 1; j < n; j ++ )
ans = (ans + dp[j][n] * dp[j][n] % P) % P;
cout << ans << "\n";
return 0;
}
E.Longest Increasing Subsequence
题意:
构造一个长度小于等于 100 的序列,使其最长上升子序列的数量等于 \(m\)。
思路:
容易想到构造 2, 1, 4, 3, 6, 5 ... \(2n\), \(2n - 1\) 这样的序列,总方案为 \(2^n\)。
将 \(m\) 转为二进制,\(a^n, a^{n - 1}, ..., a_0\),那么 \(n\) 就是二进制长度。
所以构造出来的序列已经满足了 \(2^n\)。考虑 \(a_i = 1\),要在原基础上加上这么多的最长公共子序列,可以这么操作。
2, 1, 4, 3, ..., \(2i\), \(2i - 1\), (\(b_1, b_2, ... b_k\))(添加的数), \(2(n - k + 1)\), \(2(n - k + 1) - 1\), ..., \(2n\), \(2n - 1\)。
添加了 \(k\)(后面数的数量除2) 个数,这些数要大于后面所有数,且是单调上升的,这样就可以让 \(LIS\) 的数量增加 \(2^i\) 个,同时 \(LIS\) 的长度还是 \(n\)。
其实就是用 \(k\) 个较大的数组成的一个方案替换掉后面 \(2^k\) 个方案。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
void solve(){
LL m;
cin >> m;
if (m == 1){
cout << "1\n1\n";
return;
}
vector <LL> bit;
for (int t = m; t; t >>= 1)
bit.push_back(t & 1);
vector <LL> ans;
LL len = bit.size(), cnt = 0;
for (int i = len - 2; i >= 0; i -- ){
cnt ++ ;
ans.push_back(i * 2 + 1);
ans.push_back(i * 2 + 2);
if (bit[i]){
while(cnt){
ans.push_back(0);
cnt -- ;
}
}
}
reverse(ans.begin(), ans.end());
LL n = ans.size();
cout << n << "\n";
LL num = 2 * len - 1;
for (int i = 0; i < n; i ++ ){
if (ans[i]) cout << ans[i];
else{
cout << num;
num ++ ;
}
cout << " \n"[i == n - 1];
}
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL T = 1;
cin >> T;
while(T -- ){
solve();
}
return 0;
}
G.Magic Spells
题意:
给定 \(k\) 个字符串,求在每个串中都出现的本质不同的回文串的数量。
思路:
因为 \(k <= 5\),所以对每一个字符串都建立一个 \(PAM\),然后通过 \(dfs\) 去找每棵树中都有的节点去遍历即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 3e5 + 10;
char s[N];
LL n, k, ans;
struct PAM{
LL tr[N][26], fail[N], len[N], cnt[N], idx, last;
PAM(){
fail[0] = 1;
len[1] = -1;
idx = 1;
}
void insert(char c, LL i){
LL p = get_fail(last, i);
if (!tr[p][c - 'a']){
fail[ ++ idx] = tr[get_fail(fail[p], i)][c - 'a'];
tr[p][c - 'a'] = idx;
len[idx] = len[p] + 2;
}
last = tr[p][c - 'a'];
cnt[last] = 1;
}
LL get_fail(LL u, LL i){
while(s[i - len[u] - 1] != s[i]){
u = fail[u];
}
return u;
}
}pam[5];
void dfs(vector<LL> a){
ans ++ ;
for (int i = 0; i < 26; i ++ ){
vector <LL> b(k);
for (int j = 0; j < k; j ++ ){
b[j] = pam[j].tr[a[j]][i];
if (!b[j]) break;
if (j == k - 1) dfs(b);
}
}
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
cin >> k;
for (int i = 0; i < k; i ++ ){
cin >> s;
n = strlen(s);
for (int j = 0; j < n; j ++ )
pam[i].insert(s[j], j);
}
dfs(vector<LL>(k, 0));
dfs(vector<LL>(k, 1));
cout << ans - 2 << "\n";
return 0;
}
I.The Great Wall II
题意:
有一个长为 \(n\) 的序列 \(a\),将它分成 \(k(k = 1, 2, ..., n)\) 段后,每段最大值之和最小是多少。
思路:
定义 \(dp[k][i]\) 表示用 \(k\) 段划分前 \(i\) 个数的最大值之和(后面称之为代价)的最小值。
容易得到转移方程 \(dp[k][i] = min(dp[k][j], max(a[j + 1], a[j + 2], ..., a[i])\),暴力的转移是 \(O(n^3)\),尝试优化。
如果选择了 \(a[j]\),那么所有最大值为 \(a[j]\) 的区间的 \(dp\) 值都可以转移到这里来,所以可以做一个单调递减的单调栈,维护序列中的元素划分的代价。
当 \(a[j]\) 入栈,被弹出的元素都是比它小的,即这些元素到 \(j\) 的整个区间中的最大值都是 \(a[j]\),用 \(val\) 数组记录这些元素的代价的最小值,用来进行转移。
所有弹出元素的最小代价还不是当前的答案,还漏了上一个区间的最大值的最小代价,因为是单调递减的栈,当前元素前面可能为空,也可能还有一个比它大的元素,这个元素的代价还没有计算,所以当前这个所有弹出元素求出的最小代价还要和上一个最大值区间的最小代价取一个小的才是最终答案。
例如:
4 2 1 3
3 这个数进入单调栈之后,将 2 1 弹出,记下了它们的最小值 \(x\),然后 \(x + 3\) 还要和 4 对应的最小代价取一个小的,才是 3 这个位置的最小代价。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 8e3 + 10;
LL n, a[N], f[N], g[N], stk[N], val[N], top;
int main(){
ios::sync_with_stdio(false);cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i ++ ){
cin >> a[i];
f[i] = 1e9;
}
g[0] = 1e9;
for (int k = 1; k <= n; k ++ ){
top = 0;
for (int i = 1; i <= n; i ++ ){
LL x = f[i - 1];
while(top && a[stk[top]] <= a[i]){
x = min(x, val[top]);
top -- ;
}
stk[ ++ top] = i;
val[top] = x;
g[i] = min(g[stk[top - 1]], x + a[i]);
}
for (int i = 0; i <= n; i ++ )
f[i] = g[i];
cout << f[n] << "\n";
}
return 0;
}