DP刷题记录(持续更新)
DP刷题记录
(本文例题目前大多数都选自算法竞赛进阶指南)
TYVJ1071
求两个序列的最长公共上升子序列
设\(f_{i,j}\)表示a中的\(1-i\)与b中色\(1-j\)匹配时所能构成的以\(b_j\)结尾的最长公共上升子序列的长度
考虑转移
这种序列问题是通常可以通过设计强制结尾的选择的状态来进行转移的
这样直接DP的时间复杂度为\(n^3\)不能通过本题
发现时间复杂度主要在于第二个式子
我们考虑,对于同一个\(i\),\(j\)每加一,最多只会多出一个额外决策
所以我们每次从头重新扫一遍是非常慢的选择
因为大多数的决策都是共用的,所以我们就维护目前的最优决策值,\(j\)每加以,就更新当前的最优决策
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 3005;
int a[N],b[N];
int n;
int f[N][N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int ans;
int main(){
n = read();
for(int i = 1;i <= n;++i) a[i] = read();
for(int i = 1;i <= n;++i) b[i] = read();
for(int i = 1;i <= n;++i){
int v = 0;
for(int j = 1;j <= n;++j){
if(a[i] == b[j]) f[i][j] = v + 1;
else f[i][j] = f[i - 1][j];
if(b[j] < a[i]) v = max(f[i - 1][j],v);
ans = max(ans,f[i][j]);
}
}
printf("%d\n",ans);
return 0;
}
Poj3666
给定一个数组\(a\),构造一个单调不升或者单调不降的数组\(b\),最小化\(\sum_{i = 1}^n|a_i-b_i|\)
\(b\)中的数肯定在数组\(a\)中全部出现过
我们这里只讨论单调不降的情况
首先,我们比较容易设计出一个dp
设\(f_{i}\)表示前\(i\)个数同时满足\(b_i = a_i\)的最小代价(因为\(b\)都是\(a\)中的数)
转移就枚举上一个数第一次选的位置\(k\)
\(calc(k + 1,i - 1)\)就是把\([k + 1,i - 1]\)按照顺序填上若干个\(a_k\),再填上若干个\(a_i\)
\(O(n)\)扫一下
所以这dp的时间复杂度为\(n^3\)
考虑如何优化
我们发现,转移和当前位的值息息相关,枚举就是为了寻找满足条件的解
我们索性把这个东西存到状态里
设\(f_{i,j}\)表示\(b_i\)的大小为\(j\)时的方案数
由于\(a_i\)的值过大,我们离散化即可
转移就有
就是枚举上一个填了什么
我们发现这个DP的时间复杂度仍然是\(n^3\)
但是,这个DP和上一道题有一个共同之处
\(k\)增加\(1\)决策最多增加一个
我们仍然可以通过维护最优决策的方式完成\(O(1)\)转移
那为什么第一个DP不好优化呢?因为第一个DP的公共决策不规律,而且转移的时间不好降
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 4005;
const LL INF = 1e15;
int a[N];
int b[N];
LL f[N][N];
int n;
LL ans = INF;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline void pre(){
for(int i = 1;i <= n;++i)
for(int j = 1;j <= n;++j) f[i][j] = INF;
}
int main(){
n = read();
for(int i = 1;i <= n;++i) b[i] = a[i] = read();
sort(b + 1,b + n + 1);
b[0] = unique(b + 1,b + n + 1) - b - 1;
for(int i = 1;i <= n;++i) a[i] = lower_bound(b + 1,b + b[0] + 1,a[i]) - b;
pre();
for(int i = 1;i <= n;++i){
LL v = INF;
for(int j = 1;j <= b[0];++j){
v = min(v,f[i - 1][j]);
f[i][j] = v + abs(b[j] - b[a[i]]);
}
}
for(int i = 1;i <= b[0];++i) ans = min(ans,f[n][i]);
pre();
for(int i = 1;i <= n;++i){
LL v = INF;
for(int j = b[0];j >= 1;--j){
v = min(v,f[i - 1][j]);
f[i][j] = v + abs(b[j] - b[a[i]]);
}
}
for(int i = 1;i <= b[0];++i) ans = min(ans,f[n][i]);
printf("%lld\n",ans);
return 0;
}
TYVJ1071
首先有一个比较直接的DP方式
设\(f_{i,x,y,z}\)表示
完成了前\(i\)个请求,三个服务员分别在\(x,y,z\)的位置的最优答案(我们这里强制\(x <y<z\))
转移就有(这里\(x,y,p_i\)都是无序的,实际实现时自行排序)
我们发现,处理完第\(i\)个请求,一定会有一个人在\(p_i\)所以我们记录的三个人的状态,其中有一个人是多余的
我们就设\(f_{i,j,k}\)表示处理完第\(i\)个请求,不在\(p_i\)上的两个人分别在\(x,y\)上是的最优答案
转移同上讨论即可
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 1005;
const int M = 205;
int f[2][M][M];
int c[M][M];
int n,m,now = 0;
int a[N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
m = read(),n = read();
for(int i = 1;i <= m;++i)
for(int j = 1;j <= m;++j) c[i][j] = read();
for(int i = 1;i <= n;++i) a[i] = read();
a[0] = 3;
memset(f,0x3f,sizeof(f));
f[0][1][2] = 0;
for(int i = 0;i < n;++i){
memset(f[now ^ 1],0x3f,sizeof(f[now ^ 1]));
for(int j = 1;j <= m;++j){
for(int k = 1;k <= m;++k){
if(j == k) continue;
if(j == a[i] || k == a[i]) continue;
f[now ^ 1][j][k] = min(f[now ^ 1][j][k],f[now][j][k] + c[a[i]][a[i + 1]]);
int x = min(a[i],k),y = max(a[i],k);
f[now ^ 1][x][y] = min(f[now ^ 1][x][y],f[now][j][k] + c[j][a[i + 1]]);
x = min(a[i],j),y = max(a[i],j);
f[now ^ 1][x][y] = min(f[now ^ 1][x][y],f[now][j][k] + c[k][a[i + 1]]);
}
}
now ^= 1;
}
int ans = 2e9;
for(int l = 1;l <= m;++l)
for(int r = l + 1;r <= m;++r) ans = min(ans,f[now][l][r]);
printf("%d\n",ans);
return 0;
}
acwing277
题目大意:\(n\)个人分\(m\)个物品(\(n<=30,n<=m <= 5000\)),对于第\(i\)个人来说,如果有\(a_i\)个人得到的物品数量比他多,他就会产生\(g_i \times a_i\)的怒气值,最小化怒气值之和
一个比较直接的方法是设\(f_{i,j}\)表示前\(i\)个人分配了\(j\)块饼干的最小代价
但是很明显我们不知道具体的分配方案以及后面是否可能出现还要大的人,
所以没有办法计算代价,也就是说,直接这样DP是不可行的
我们想一下,如果对于两个人\(x,y\),如果存在\(g_x >= g_y\)但\(x\)分配到的物品比\(y\)少,那么把他们两个人分配到的数量交换一下,一定不会变劣,这也就启示我们,单位怒气值大的人分配的数量不能低于单位怒气值小的人我们首先把人按照\(g\)从大到小排序
DP的时候我们要保证分配的树木是单调不增的
(这种类型的DP貌似都有一个非常神奇的转移方式)
我们设\(f_{i,j}\)表示前\(i\)个人分配了\(j\)块饼干的最小怒气值
转移
前面的\(j - i\)相当于给前\(i\)个人每个人分配一个物品,相对大小不变所以不会影响怒气值(为了保证单调不减)
后面的就是相当于,枚举最后面的几个人被分配到了一个物品(之后可通过第一个操作再次增加数量)
通过简单前缀和优化可以做到\(O(n^2m)\)
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 31;
const int M = 5005;
const LL INF = 1e15;
LL f[N][M];
struct node{
int v;
int id;
}a[N];
pii pre[N][M];
int n,m;
int ans[N];
LL sum[N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline bool cmp(node x,node y){
return x.v > y.v;
}
inline void print(int x,int y){
// printf("%d %d %d %d\n",x,y,pre[x][y].fi,pre[x][y].se);
if(!x){
for(int i = 1;i <= n;++i) printf("%d ",ans[i]);
return ;
}
if(pre[x][y].fi == x){
for(int i = 1;i <= x;++i) ans[a[i].id]++;
}
else{
for(int i = pre[x][y].fi + 1;i <= x;++i) ans[a[i].id]++;
}
print(pre[x][y].fi,pre[x][y].se);
}
int main(){
n = read(),m = read();
for(int i = 1;i <= n;++i) a[i].v = read(),a[i].id = i;
sort(a + 1,a + n + 1,cmp);
for(int i = 1;i <= n;++i) sum[i] = sum[i - 1] + a[i].v;
for(int i = 0;i <= n;++i)
for(int j = 0;j <= m;++j) f[i][j] = INF;
f[0][0] = 0;
for(int i = 1;i <= n;++i){
for(int j = 1;j <= m;++j){
if(j >= i && f[i][j - i] < f[i][j]) f[i][j] = f[i][j - i],pre[i][j] = mk(i,j - i);
for(int k = 0;k < i;++k){
if(j - (i - k) < 0) continue;
if(f[i][j] > f[k][j - (i - k)] + (sum[i] - sum[k]) * k){
f[i][j] = f[k][j - (i - k)] + (sum[i] - sum[k]) * k;
pre[i][j] = mk(k,j - (i - k));
}
}
}
}
printf("%lld\n",f[n][m]);
print(n,m);
return 0;
}
CF1215E
题目大意:给定\(n(n\le 4\times 10^5)\)个物品,第\(i\)个物品颜色为\(a_i (a_i \le 20)\),每次可以交换相邻的两个物品,最小化使得所有颜色相同的物品相邻的交换次数
首先,\(a_i\)这么小,第一反应就是状压DP
我们考虑设\(f_S\)表示把\(S\)这个状态的颜色排列在前面的最小代价
每次考虑一个新的颜色\(i\),转移到\(f_{S | (1 << i)}\)即可
但是现在有一个非常严重的问题,如何计算把\(i\)接在\(S\)这个集合后面的贡献
我们想,如果颜色之间有大小关系,对于\((i,j)\)来说,如果颜色\(i\)大于颜色\(j\)
那么把\(j\)放到\(i\)的前面贡献,就是\((i,j)\)这两种颜色贡献的逆序对的个数
(因为\(i\)后面的每一个\(j\),都会与\(i\)产生一次交换)
我们就可以预处理一个数组\(g_{i,j}\)表示把\(j\)这种颜色全部排在\(i\)颜色的前面的代价
对于一个新颜色\(i\)
贡献就是
直接DP就好了
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 5e5 + 3;
const LL INF = 1e15;
LL f[(1 << 21) + 3];
int n,m;
LL sum[N][25];
LL g[101][101];
int a[N],b[N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
n = read();
for(int i = 1;i <= n;++i) b[i] = a[i] = read();
sort(b + 1,b + n + 1);
b[0] = unique(b + 1,b + n + 1) - b - 1;
for(int i = 1;i <= n;++i) a[i] = lower_bound(b + 1,b + b[0] + 1,a[i]) - b - 1;
for(int i = n;i >= 1;--i){
for(int j = 0;j < b[0];++j) sum[i][j] = sum[i + 1][j];
sum[i][a[i]]++;
}
for(int i = 1;i <= n;++i){
for(int j = 0;j < b[0];++j){
if(j == a[i]) continue;
g[a[i]][j] += sum[i][j];
}
}
for(int i = 0;i < (1 << b[0]);++i) f[i] = INF;
for(int i = 0;i < b[0];++i) f[1 << i] = 0;
for(int i = 1;i < (1 << b[0]);++i){
for(int k = 0;k < b[0];++k){
if(i & (1 << k)) continue;
LL cost = 0;
for(int j = 0;j < b[0];++j) if(i & (1 << j)) cost += g[k][j];
f[i | (1 << k)] = min(f[i] + cost,f[i | (1 << k)]);
}
}
cout << f[(1 << b[0]) - 1] << endl;
return 0;
}
CF455A
比较简单的DP
我们设\(f_i\)表示操作完\(1 - i\)的最大收益,转移显然
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 2e5 + 3;
int a[N];
int sum[N];
LL f[N];
int n;
LL ans;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
n = read();
for(int i = 1;i <= n;++i) a[i] = read(),sum[a[i]]++;
for(int i = 1;i <= 100000;++i){
if(sum[i]) f[i] = max(f[i - 1],f[i - 2] + 1ll * i * sum[i]);
else f[i] = f[i - 1];
ans = max(ans,f[i]);
}
cout << ans << endl;
return 0;
}
CF1051D
题目大意,给一个\(2\times n\)的矩阵,黑白染色,使得最后同色连通块的数量为\(k\)的方案数
还是比较直接的DP的,我们考虑
设\(f_{i,j,0/1,0/1}\)表示前\(i\)列,连通块为\(j\)且上面的是\(0/1\)下面的是\(0/1\)的方案数
转移的话= =
分类讨论一下吧
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 2e3 + 3;
const LL mod = 998244353;
LL f[N][N][2][2];
int n,k;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline int mo(int x){
if(x >= mod) x -= mod;
return x;
}
int main(){
n = read();k = read();
f[1][1][0][0] = 1;
f[1][1][1][1] = 1;
f[1][2][0][1] = 1;
f[1][2][1][0] = 1;
for(int i = 1;i <= n;++i){
for(int j = 1;j <= k;++j){
(f[i + 1][j][0][0] += (f[i][j][0][0] + f[i][j][0][1] + f[i][j][1][0])) %= mod;
(f[i + 1][j][1][1] += (f[i][j][1][1] + f[i][j][1][0] + f[i][j][0][1])) %= mod;
(f[i + 1][j][0][1] += f[i][j][0][1]) %= mod;
(f[i + 1][j][1][0] += f[i][j][1][0]) %= mod;
(f[i + 1][j + 1][0][0] += f[i][j][1][1]) %= mod;
(f[i + 1][j + 1][1][1] += f[i][j][0][0]) %= mod;
(f[i + 1][j + 1][0][1] += (f[i][j][0][0] + f[i][j][1][1])) %= mod;
(f[i + 1][j + 1][1][0] += (f[i][j][0][0] + f[i][j][1][1])) %= mod;
(f[i + 1][j + 2][0][1] += f[i][j][1][0]) %= mod;
(f[i + 1][j + 2][1][0] += f[i][j][0][1]) %= mod;
}
}
printf("%lld\n",(f[n][k][0][0] + f[n][k][1][1] + f[n][k][1][0] + f[n][k][0][1]) % mod);
return 0;
}
Luogu4342
题面太长了,不放了
很明显,按照题目要求枚举每一条边被拆掉
然后区间DP
设\(f_{l,r}\)表示合并\([l,r]\)这个区间可以得到的最大值,\(g_{l,r}\)表示合并\([ l,r]\)的最小值
根据加法乘法的基本运算法则进行转移
(最大值可能由两个最小值相乘得到)
另外不要忘记数组初始化
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 205;
int f[N][N],g[N][N];
int n;
char t[N];
vector <int> G;
int a[N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
memset(f,-0x3f,sizeof(f));
memset(g,0x3f,sizeof(g));
ios::sync_with_stdio(false);
cin >> n;
for(int i = 1;i <= n;++i){
cin >> t[i] >> a[i];
// cout << t[i] << ' ' << a[i] << ' ';
t[i + n] = t[i];
a[i + n] = a[i];
f[i][i] = g[i][i] = a[i];
f[i + n][i + n] = g[i + n][i + n] = a[i];
}
n <<= 1;
for(int len = 2;len <= n;++len){
for(int l = 1;l + len - 1 <= n;++l){
int r = l + len - 1;
for(int k = l + 1;k <= r;++k){
if(t[k] == 't'){
// cout << 1 << endl;
f[l][r] = max(f[l][r],f[l][k - 1] + f[k][r]);
g[l][r] = min(g[l][r],g[l][k - 1] + g[k][r]);
}
else{
f[l][r] = max(f[l][r],g[l][k - 1] * g[k][r]);
f[l][r] = max(f[l][r],f[l][k - 1] * f[k][r]);
f[l][r] = max(f[l][r],g[l][k - 1] * f[k][r]);
f[l][r] = max(f[l][r],f[l][k - 1] * g[k][r]);
g[l][r] = min(g[l][r],g[l][k - 1] * g[k][r]);
g[l][r] = min(g[l][r],f[l][k - 1] * f[k][r]);
g[l][r] = min(g[l][r],g[l][k - 1] * f[k][r]);
g[l][r] = min(g[l][r],f[l][k - 1] * g[k][r]);
}
}
}
}
int ans = -99999999;
for(int l = 1;l <= n / 2;++l){
int r = l + (n >> 1) - 1;
ans = max(ans,f[l][r]);
}
for(int l = 1;l <= n / 2;++l){
int r = l + (n >> 1) - 1;
if(f[l][r] == ans) G.push_back(l);
}
cout << ans << '\n';
for(int i = 0;i < (int)G.size();++i) cout << G[i] << ' ';
return 0;
}
CF1219E1E2
首先观察发现\(n\)特别小,当一个数小于等于\(20\)的时候第一反应就是要考虑状压
一个特别重要的性质
我们把所有的列按照这一列的最大值从大到小跑徐之后
最多只有前\(n\)列有用
也就是说
$n\times m \(的矩阵变成了\)n \times n$的
我们设\(f_{i,S}\)表示前\(i\)列,\(S\)这个状态对应的行的最大值已经确定的最大贡献,
\(g_{i,S}\)表示从第\(i\)列取出能够表示\(S\)这个集合的最大值
转移
记下来考虑怎么求\(g\)
由于存在轮换的存在
我们枚举一个集合,然后把这个集合能够表示的所有的状态都尝试用这个集合的值去更新
比如
\(1101\)能够表示\(1101\),\(1110\),\(1011\),\(0111\)
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int M = 21;
const int N = 2003;
int a[M][N];
int b[M][M];
int n,m;
int f[13][(1 << 13) + 5];
int g[13][(1 << 13) + 3];
struct node{
int maxx;
int id;
}row[N];
inline bool cmp(node x,node y){
return x.maxx > y.maxx;
}
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
int T = read();
while(T--){
memset(g,0,sizeof(g));
memset(f,0,sizeof(f));
memset(row,0,sizeof(row));
n = read(),m = read();
for(int i = 1;i <= n;++i){
for(int j = 1;j <= m;++j){
a[i][j] = read();
row[j].maxx = max(row[j].maxx,a[i][j]);
row[j].id = j;
}
}
sort(row + 1,row + m + 1,cmp);
m = min(n,m);
for(int i = 1;i <= n;++i){
for(int j = 1;j <= m;++j)
b[i][j] = a[i][row[j].id];
}
for(int j = 1;j <= m;++j){
for(int i = 1;i < (1 << n);++i){
int res = i;
int sum = 0;
for(int h = 0;h < n;++h) if(res & (1 << h)) sum += b[h + 1][j];
for(int k = 0;k < n;++k){
g[j][res] = max(g[j][res],sum);
int nn = res;
res = ((nn >> (n - 1)) & 1) | ((nn << 1) & ((1 << n) - 1));
}
}
}
for(int i = 0;i < m;++i){
for(int j = 0;j < (1 << n);++j){
int S = ((1 << n) - 1) ^ j;
f[i + 1][j] = max(f[i + 1][j],f[i][j]);
for(int son = S;son;son = (son - 1) & S){
f[i + 1][j | son] = max(f[i + 1][j | son],f[i][j] + g[i + 1][son]);
}
}
}
printf("%d\n",f[m][(1 << n) - 1]);
}
return 0;
}
CF1061C
题目大意:
给定数组\(a\),求有多少子序列\(b\)满足\(\forall i,i|b_i\)
首先,对于一个数\(x\),他的因子最多有\(\sqrt{x}\)个
所以
我们设\(f_i\)表示长度为\(i\)的子序列的个数
每次枚举所有质因子暴力转移
在转移的时候要从大往小枚举质因子
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 2e5 + 3;
const LL mod = 1e9 + 7;
const int M = 1e6 + 3;
int a[N];
int n;
LL f[M];
vector <int> G;
inline int up(int x){
if(x >= mod) x -= mod;
return x;
}
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline int cmp(int x,int y){
return x > y;
}
inline void work(int x){
G.clear();
for(int i = 1;i * i <= a[x];++i){
if(a[x] % i == 0){
G.push_back(i);
if(i != a[x] / i) G.push_back(a[x] / i);
}
}
sort(G.begin(),G.end(),cmp);
for(int i = 0;i < (int)G.size();++i) f[G[i]] = up(f[G[i]] + f[G[i] - 1]);
}
int main(){
n = read();
for(int i = 1;i <= n;++i) a[i] = read();
f[0] = 1;
for(int i = 1;i <= n;++i) work(i);
int ans = 0;
for(int i = 1;i <= 1000000;++i) ans = up(ans + f[i]);
printf("%d\n",ans);
return 0;
}
ZR991
神仙基础树形DP?
我真的不知道哪里基础了
我们设\(f_{i,0/1,0/1}\)表示\(i\)为根的子树中的理论状态和实际状态
首先应该知道一个事实,理论状态和实际状态是相互独立的,否则下面的转移会很难办
首先,为了方便表示,我们设\(x\)表示根,设\(y\)表示\(x\)的所有儿子
那么\(f_{x,0,0}\)的值我们是非常好求的
因为理论全\(1\)要求儿子理论全\(0\),实际全\(1\)要去儿子实际全\(0\)
所以一结合,理论实际都要全\(1\)
接下来想如何去求\(f_{x,0,1}\)
这东西非常难办,因为\(1\)要求只要有一个\(0\)存在即可
我们考虑正难则反,用全部的减去不合法的
也就是
\(\prod (f_{y,1,0} + f_{y,1,0})\)是理论状态为\(0\)的总方案数,\(f_{x,0,0}\)是理论状态为\(0\),实际状态为\(0\)的总方案数
那么前者减后者就是理论状态为\(0\),实际状态为\(1\)的总方案数
同理
\(f_{x,1,0}\)我们也可以使用类似的方式求出
道理同上
最后\(f_{x,1,1,}\)我们已经没有一维可以唯一去确定转移方向
没有问题,因为我们已经知道了除了\(f_{x,1,1}\)以外的所有答案,用总的方案数减去不合法的
另外一个需要注意的小地方
我们在减掉不合法的之前
\(f_{x,1,0}\)和\(f_{x,0,1}\)表示的都是全集,那么初值
应该是
\(f_{x,1,1} = 2^{g_i - d_i}\),\(f_{0,0} = 1\)
\(g_i\)是题目中给定的接口数量,\(d_i\)是这个点的儿子数量
因为在开始\(f_{ x,1,1,}\)包含\(f_{x,0,0}\)所以赋上面的初值应该不难理解
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
#define min std::min
#define max std::max
const int N = 4e5 + 3;
const LL mod = 998244353;
struct edge{
int to;
int nxt;
}e[N << 1];
int n,tot,rt;
LL g[N];
LL rest[N];
int head[N];
int fa[N];
int flag[N];
LL d[N];
LL f[N][2][2];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline void add(int x,int y){
e[++tot].to = y;
e[tot].nxt = head[x];
d[x]++;
head[x] = tot;
}
inline LL mo(LL x){
if(x >= mod) x -= mod;
if(x < mod) x += mod;
return x;
}
inline LL quick(LL x,LL y){
LL res = 1;
while(y){
if(y & 1) res = res * x % mod;
y >>= 1;
x = x * x % mod;
}
return res;
}
//f[i][0/1][0/1]表示i的子树中,理论状态为0/1,实际状态为0/1的方案数
inline void dfs(int x){
rest[x] = g[x] - d[x];
f[x][0][1] = f[x][1][0] = 1;
f[x][0][0] = 1;
f[x][1][1] = quick(2,rest[x]);
for(int i = head[x];i;i = e[i].nxt){
int y = e[i].to;
dfs(y);
rest[x] += rest[y];
f[x][0][0] = f[x][0][0] * f[y][1][1] % mod;
f[x][0][1] = f[x][0][1] * (f[y][1][1] + f[y][1][0]) % mod;
f[x][1][0] = f[x][1][0] * (f[y][1][1] + f[y][0][1]) % mod;
f[x][1][1] = f[x][1][1] * (f[y][0][0] + f[y][0][1] + f[y][1][0] + f[y][1][1]) % mod;
}
f[x][0][1] = mo(f[x][0][1] - f[x][0][0]);
f[x][1][0] = mo(f[x][1][0] - f[x][0][0]);
f[x][1][1] = ((f[x][1][1] - f[x][1][0] - f[x][0][1] - f[x][0][0]) % mod + mod) % mod;
if(flag[x] == 1){
f[x][0][1] = mo(f[x][0][1] + f[x][0][0]);
f[x][0][0] = 0;
f[x][1][1] = mo(f[x][1][1] + f[x][1][0]);
f[x][1][0] = 0;
}
if(flag[x] == 0){
f[x][0][0] = mo(f[x][0][1] + f[x][0][0]);
f[x][0][1] = 0;
f[x][1][0] = mo(f[x][1][1] + f[x][1][0]);
f[x][1][1] = 0;
}
}
int main(){
n = read();
for(int i = 1;i <= n;++i){
fa[i] = read();
if(fa[i]) add(fa[i],i);
else rt = i;
g[i] = read();
flag[i] = read();
}
dfs(rt);
printf("%lld\n",mo(f[rt][0][0] + f[rt][1][1]) * quick(quick(2,rest[rt]),mod - 2) % mod);
return 0;
}
ZR1007
题目大意:给定一个无向图,把这个无向图中的每一条边定向,使得最长路最短
\(n<=17,m<=\frac{n\times (n - 1)}{ 2}\)
题目范围就提醒了本题做法,状压DP
首先关于有向图的最长路径.对于一个最长路径为\(x\)的有向图一定存在一种分层方案分成\(x + 1\)层,使得所有的边都在层层之间,而且同层之间没有边(这个应该还是比较显然的)
也就是说
我们要让最长路径最短,实质是要把这个图分层,使得同层之间没有边
要求分得层数尽量少
我们发现层的本质是一个独立集
题目就转化成了选出最少的独立集个数,使得他们的并是全集
我们可以用\(2^n \times n^2\)的时间复杂度预处理出每个子集是否是独立集
之后设\(f_{T}\)表示已经选择了\(T\)这个集合的最小层数
转移比较明显
我们在考虑一种情况
就是这样
很明显这个图的答案为\(1\),但是我们很有可能分三层
但是我们还是会枚举到一个最优的答案去更新
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
#define min std::min
#define max std::max
const int N = 18;
int f[(1 << N) + 5];
int n,m;
bool e[N][N];
int s[N],tot;
bool g[(1 << N) + 5];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline bool check(){
for(int i = 1;i <= tot;++i)
for(int j = i + 1;j <= tot;++j)
if(e[s[i]][s[j]]) return 0;
return 1;
}
int main(){
memset(f,0x3f,sizeof(f));
n = read(),m = read();
for(int i = 1;i <= m;++i){
int x = read(),y = read();
e[x][y] = e[y][x] = 1;
}
for(int i = 1;i < (1 << n);++i){
tot = 0;
for(int j = 0;j < n;++j) if(i & (1 << j)) s[++tot] = j + 1;
g[i] = check();
}
f[0] = 0;
for(int i = 1;i < (1 << n);++i){
for(int S = i;S;S = (S - 1) & i){
if(!g[S]) continue;
f[i] = min(f[i],f[i ^ S] + 1);
}
}
printf("%d\n",f[(1 << n) - 1] - 1);
return 0;
}
AcWing 284
题目大意,一棵\(n\)个节点,结构未定的树,每个点有一个颜色,现在给定这棵树的颜色欧拉序,求有多少棵树结构满足给定条件\((n \le 300)\)
首先,一个子树的欧拉序对应的一定对应着一个区间,很明显,枚举断点的个数和位置的做法是显然不可取的
这样我们就靠考虑一个比较好的定义去避免这种情况
我们设\(f_{l,r}\)表示\([l,r ]\)这个区间能代表的树的结构
我们每次枚举断点转移(再次讲过这个点的位置)
这张看上去很对,但是会造成重复计数
所以直接枚举断点位置的方法是不可取
所以
我们转移的时候强制枚举第一颗子树的所在区间,其余的子树作为一个子问题
就是
我们每次都只会确定一颗子树,上面直接枚举断点的方式可能会以此确定一片子树
转移有
大体意思就是我们枚举一个\(k\),这个\(k\)是走完这个子树再次回来的地方
则\([l + 1,k - 1]\)就可以视为是第一棵子树,其余的区间也变成了一个子问题
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
#define min std::min
#define max std::max
const int N = 505;
const LL mod = 1e9;
LL f[N][N];
char s[N];
int n;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline LL dp(int l,int r){
if(l > r) return 0;
if(l == r) return 1;
if(f[l][r] != -1) return f[l][r];
if(s[l] != s[r]) return f[l][r] = 0;
LL sum = 0;
for(int k = l + 2;k <= r;++k)
sum = (sum + dp(l + 1,k - 1) * dp(k,r)) % mod;
return f[l][r] = sum;
}
int main(){
scanf("%s",s + 1);
n = strlen(s + 1);
memset(f,-1,sizeof(f));
printf("%lld\n",dp(1,n));
return 0;
}
LuoguP2014
树形DP基础
即使存在森林,我们也可以随便设一个节点让他成为所谓根的父亲
称这个点为超级根,现在问题就变成了在超级根上做有限制的树上背包
因为强制根必选
所以我们设\(f_{i,j}\)表示在\(i\)为根的子树中选择\(j\)个点的最大代价(必须选根)
但我们在DP的之后,直接DP不能保证根必选
所以我们DP的时候,就在子树内选择最多\(m\)
个点(强制根不选)
最后在强制根选(也就是把根的贡献加进去就好
inline void dfs(int x){
size[x] = 1;
for(int i = 0;i < (int)G[x].size();++i){
int y = G[x][i];
dfs(y);
for(int u = min(size[x],m);u >= 0;--u){
for(int v = min(size[y],m - u);v >= 0;--v){
f[x][u + v] = max(f[x][u] + f[y][v],f[x][u + v]);
}
}
size[x] += size[y];
}
for(int i = m + 1;i >= 1;--i) f[x][i] = f[x][i - 1] + s[x];
}
POJ3585
题目大意,给你一个一棵树,选定一个源点,给你每条边的容量,把源点作为根的叶子结点看做汇点最大化流量(\(n \le 2\times 10^5\))
题目一幅网络流的样子
显然枚举源点跑网络流显然不可做(时间复杂度)
考虑若何优化这过程
枚举源点,设\(f_i\)表示\(i\)子树内以\(i\)为根的最大流量
每次枚举根跑一遍DP就好了
时间复杂度为\(O(n^2)\)
所以此类树形DP,需要一个时间复杂度更加优秀的做法
这要就用到二次扫描,也叫up and down思想
我们设\(f_i\)定义同上,同时设\(g_i\)表示\(i\)点向子树外延伸的最大流量
\(f\)我们很好求(\(w\)为边权)
现在想一下,在已经知道\(f\)数组的前提下如果求\(g\)
首先\(f_{fa_x}\)中有\(x\)的子树的贡献,\(f_{fa_x} - \max(w,f_x)\)就是\(fa_x\)的子树内除了\(x\)这棵子树以外的贡献
再加上\(g_{fa_x}\)就是\(x\)向子树外的贡献了,再和流量取个最小值即可
另外遇到度数为1的点要特殊处理即可
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
#define min std::min
#define max std::max
const int N = 3e5 + 5;
const LL INF = 1e16;
struct edge{
int to;
LL data;
int nxt;
}e[N << 1];
int head[N];
LL f[N],g[N];
int d[N];
int n,m,tot;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline void add(int x,int y,LL z){
e[++tot].to = y;
e[tot].data = z;
e[tot].nxt = head[x];
head[x] = tot;
d[y]++;
}
inline void dfs(int x,int fa){
bool ok = 1;
for(int i = head[x];i;i = e[i].nxt){
int y = e[i].to;
if(y == fa) continue;
ok = 0;
dfs(y,x);
f[x] += min(f[y],e[i].data);
}
if(ok) f[x] = INF;
}
inline void sfd(int x,int fa){
for(int i = head[x];i;i = e[i].nxt){
int y = e[i].to;
if(y == fa) continue;
if(d[x] != 1) g[y] += min(g[x] + f[x] - min(f[y],e[i].data),e[i].data);
else g[y] = e[i].data;
sfd(y,x);
}
}
int main(){
int T = read();
while(T--){
tot = 0;
memset(head,0,sizeof(head));
memset(f,0,sizeof(f));
memset(d,0,sizeof(d));
memset(g,0,sizeof(g));
n = read();
for(int i = 1;i < n;++i){
int x = read(),y = read(),z = read();
add(x,y,z);
add(y,x,z);
}
dfs(1,0);
sfd(1,0);
for(int i = 1;i <= n;++i) if(f[i] == INF) f[i] = 0;
LL ans = 0;
for(int i = 1;i <= n;++i) ans = max(ans,f[i] + g[i]);
printf("%lld\n",ans);
}
return 0;
}
Luogu1941NOIP2014飞扬的小鸟
题目太长,就不写简要了
首先这个题一副非常不可做的样子之后仔细想一下
我们设\(f_{i,j}\)表示到达第\(i\)列高度为\(j\)时的最小代价
转移貌似挺简单的
首先下降:
由于上升可以再同一时刻上升无数次,相当于无限背包
最后再把撞到管子上的赋值为\(\infty\)就好了
为什么不在DP的时候通过控制范围来避免撞到管子呢?
考虑一下这种情况
因为我们完全背包DP转移是每一步都依赖与上一步
如果一个位置至少跳两下才能达到要求,那么跳一步的就不合法,导致在转移时认为跳两步也不合法
所以
DP的转移方程不要想当然,要每一个都有所依据,综合考虑全部情况
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii std::pair<int,int>
#define mk std::make_pair
#define fi first
#define se second
#define min std::min
#define max std::max
const int N = 10005;
const int M = 2005;
int f[N][M];
int g[M];
int n,m,k;
pii p[N];
struct node{
int xi;
int yi;
}a[N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
n = read(),m = read(),k = read();
for(int i = 0;i < n;++i) a[i].xi = read(),a[i].yi = read();
for(int i = 0;i <= n;++i) p[i] = mk(0,m + 1);
for(int i = 1;i <= k;++i){
int x = read();
p[x].fi = read();
p[x].se = read();
}
memset(f,0x3f,sizeof(f));
bool ok = 0;
for(int i = p[0].fi + 1;i < p[0].se;++i) f[0][i] = 0,ok = 1;
if(!ok) return printf("0\n0\n") * 0;
int sum = (p[0] == mk(0,m + 1)) ? 0 : 1;
for(int i = 0;i < n;++i){
for(int j = 1;j < m + 1;++j){
int t = min(m,j + a[i].xi);
f[i + 1][t] = min(f[i + 1][t],min(f[i][j] + 1,f[i + 1][j] + 1));
}
for(int j = 1;j < m + 1;++j){
int t = j - a[i].yi;
if(t > p[i + 1].fi && t < p[i + 1].se)
f[i + 1][t] = min(f[i + 1][t],f[i][j]);
}
for(int j = 1;j <= p[i + 1].fi;++j) f[i + 1][j] = 0x3f3f3f3f;
for(int j = p[i + 1].se;j <= m;++j) f[i + 1][j] = 0x3f3f3f3f;
int now = 0x3f3f3f3f;
for(int j = p[i + 1].fi + 1;j < p[i + 1].se;++j) now = min(now,f[i + 1][j]);
//system("pause");
if(now > 999999999){
printf("0\n%d\n",sum);
return 0;
}
if(p[i + 1] != mk(0,m + 1)) sum++;
}
int ans = 0x3f3f3f3f;
for(int i = 1;i <= m;++i) ans = min(ans,f[n][i]);
printf("1\n%d\n",ans);
return 0;
}
POJ2228
题目大意:一天有\(n\)个小时,总共要睡\(m\)个小时,在第\(i\)个小时熟睡可以获得\(w_i\)的体力
睡觉不需要连续,但是每次连续睡觉的第一个小时不会获得体力(第一天的第\(n\)小时后就是第二天的第\(1\)小时)
怎样规划使得每一天获得的体力最多
题目中的意思也就是说存在环,使得\(1\)小时也可以进行熟睡
我们先考虑只有一天怎么做
设\(f_{i,j,0/1}\)表示前\(i\)小时睡了\(j\)个小时,第\(i\)小时是否在睡觉恢复的最大体力
边界有
\(f_{1,0,0} = 0,f_{1,1,1} = 0\)(因为只有一天无论如何也不能在\(1\)时刻熟睡)
这应该比较明显
减下来想,有环和没有环的唯一区别就是能够否在\(1\)时刻就熟睡
我们就强制在\(1,n\)时刻睡觉在强制做一遍DP取最大值
所以,解决环类为题的方法一般为断环为链,但是这种题目
注意观察有环和无环的区别也是解决某些环类问题的关键
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
#define min std::min
#define max std::max
const int N = 4505;
int f[2][N][2];
int g[2][N][2];
int n,m,now;
int w[N];
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
int main(){
n = read(),m = read();
for(int i = 1;i <= n;++i) w[i] = read();
memset(f,-0x3f,sizeof(f));
memset(g,-0x3f,sizeof(g));
f[0][0][0] = f[0][1][1] = 0;g[0][1][1] = w[1];
for(int i = 1;i < n;++i){
for(int j = 0;j <= m;++j){
f[now ^ 1][j + 1][1] = max(f[now][j][0],f[now][j][1] + w[i + 1]);
f[now ^ 1][j][0] = max(f[now][j][0],f[now][j][1]);
g[now ^ 1][j + 1][1] = max(g[now][j][0],g[now][j][1] + w[i + 1]);
g[now ^ 1][j][0] = max(g[now][j][0],g[now][j][1]);
}
now ^= 1;
}
printf("%d\n",max(max(f[now][m][0],f[now][m][1]),g[now][m][1]));
return 0;
}
Acwing289
环形公路上有\(n\)个点,编号_i~j_的距离$dist = $$\min(|i - j|,N - |i - j|)$(顺时针或者逆时针走过去)
在\(i,j\)运输货物的代价是\(a_i+a_j+dist_{i,j}\)
求哪两个点运输货物代价最大\(n \le 10^6\)
首先,这类环的问题特别是发现断点没有什么特殊性质的时候,就要考虑断环为链
先把数组倍长,然后考虑
如果我们强制一个方向\(i\)到\(j\)去运输\((i > j)\)很明显最短距离必须要满足\(i - j\le \frac{n}{2}\)
(否则就不是最短)
要求最大化\(a_i+a_j +(i - j)\)
我们就用单调队列去维护前\([i - \frac{n}{2},i]\)\(a_i - i\)的最大值即可
CF1223D
给定一个数组\(a\) ,每次可以选择一个数字 $ x $ 并且将所有等于 \(x\) 的数全部放到开头或者末尾,求最小操作步数使得数组单调不减
首先,上界肯定是\(n\),这种情况只会出现于我们不得不把所有的元素都移动一遍
但是很明显,大部分情况我们都不需要移动所有的元素,发现不需要移动的元素必须满足已经有序,并且必须是连续的区间,那么我们现在要求出来最长的不需要移动的区间,用\(n\)减去便是答案
那么我们想怎么判断一个连续的区间合法?
我们设\(fir_i\)为\(i\)第一次出现的位置,\(las_i\)为\(i\)最后一次出现的位置
对于\(i\)和\(i + 1\),他们合法当且仅当\(las_i < fir_{i + 1}\)
那么我们就设\(f_{i}\)表示以\(i\)开头最大向后延伸的区间长度
转移就看上面式子是否成立就好了
#include<cstdio>
#include<cctype>
#include<algorithm>
#include<iostream>
#include<vector>
using namespace std;
const int N = 3e5 + 3;
int a[N],b[N],f[N];
int fir[N],las[N];
int n,T;
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i = 1;i <= n;++i){
f[i] = 0;
fir[i] = 0;
las[i] = 0;
}
for(int i = 1;i <= n;++i){
scanf("%d",&a[i]);
b[i] = a[i];
}
sort(b + 1,b + n + 1);
b[0] = unique(b + 1,b + n + 1) - b - 1;
for(int i = 1;i <= n;++i){
a[i] = lower_bound(b + 1,b + b[0] + 1,a[i]) - b;
if(!fir[a[i]]) fir[a[i]] = i;
las[a[i]] = i;
}
f[b[0]] = 1;
for(int i = b[0] - 1;i >= 1;--i) f[i] = las[i] < fir[i + 1] ? f[i + 1] + 1 : 1;
int ans = 0;
for(int i = 1;i <= n;++i) ans = max(ans,f[i]);
printf("%d\n",b[0] - ans);
}
return 0;
}
ZR971
题目大意:给定数组\(a\)和\(b\)问有多少种方案使得在\(a\)中取出一个子序列字典序比在\(b\)中取出的大.(两个方案不同,当且仅当来两个方案在\(a\)中取得子序列或者在\(b\)中取的子序列长得不一样)$|a|,|b| \le 5000 $
长得不一样就是本质不同,我们设\(a\)中取\(S_1\),\(b\)中\(S_2\),发现答案贡献来自于两部分,一部分是\(|S_1| > |S_2|\),另一部分是\(|S_1| = |S _2|\)同时\(S_1\)字典序较大
第一个问题我们可以通过求一个数列本质不同的子序列来计算答案,为了防止DP时重复计数,我们强制转移的时候对于同一个数字我们直接选择第一次出现的位置,这样能保证所有的子序列在第一次出现的时候就被算到
我们设\(f_{i,j}\)表示以\(i\)结尾长度为\(j\)的本质不同的个数
设\(pre_i\)表示\(a_i\)上一次出现的位置
为了防止重复计数,如果\(a_i\)这个数出现过,我们转移的位置不能在\(pre_{i}\)前面(会造成重复计数)
所以
通过前缀和优化我们能够在平方时间内求出\(f\)数组(其实直接维护前缀和就好了,\(f\)数组只是方便理解)
接着考虑如何统计长度相同但是字典序大的方案数
设\(f_{i,j}\)表示第\(i\)位匹配了第\(j\)位,且长度相同的序列,字典序大小相同的总方案数
设\(g_{i,j}\)表示第\(i\)为匹配了第\(j\)为,且长度相同,字典序\(S_1 > S_ 2\)的总方案数
转移我们考虑向上面那样,为了避免重复计数,我们还是都要在最早出现的时候就计入贡献
对于任意的\(i,j\)我们有
如果\(a_i = b_j\)我们有
如果\(a_i > b_j\),有
之后发现上面的东西能够通过二位前缀和去优化
(同理,\(f\),\(g\)数组并不需要专门维护,只维护前缀和数组即可)
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#include<set>
#include<map>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 5005;
const LL mod = 998244353;
int s1[N][N],s2[N][N];
int sf[N][N],sg[N][N];
int prea[N],preb[N];
int v[N],a[N],b[N];
int n,m;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline void mo1(int &x){
if(x >= mod) x -= mod;
}
inline void mo2(int &x){
if(x < 0) x += mod;
}
inline int get(int val[][N],int a,int b,int c,int d){
int res = val[c][d];
if(a) res -= val[a - 1][d],mo2(res);
if(b) res -= val[c][b - 1],mo2(res);
if(a && b) res += val[a - 1][b - 1],mo1(res);
return res;
}
int main(){
n = read(),m = read();
for(int i = 1;i <= n;++i){
a[i] = read();
prea[i] = v[a[i]];
v[a[i]] = i;
}
memset(v,0,sizeof(v));
for(int i = 1;i <= m;++i){
b[i] = read();
preb[i] = v[b[i]];
v[b[i]] = i;
}
s1[0][0] = 1;
for(int i = 1;i <= n;++i){
s1[i][0] = s1[i - 1][0];
for(int j = 1;j <= n;++j){
int v = prea[i] ? s1[i - 1][j - 1] - s1[prea[i] - 1][j - 1] : s1[i - 1][j - 1];
mo2(v);
s1[i][j] = s1[i - 1][j] + v;
mo1(s1[i][j]);
}
}
s2[0][0] = 1;
for(int i = 1;i <= m;++i){
s2[i][0] = s2[i - 1][0];
for(int j = 1;j <= m;++j){
int v = preb[i] ? s2[i - 1][j - 1] - s2[preb[i] - 1][j - 1] : s2[i - 1][j - 1];
mo2(v);
s2[i][j] = s2[i - 1][j] + v;
mo1(s2[i][j]);
}
}
// for(int i = 0;i <= n;++i) printf("%d ",s1[n][i]);puts("");
LL ans = 0;
for(int i = 1;i <= n;++i){
for(int j = 1;j < i;++j){
ans = (ans + 1ll * s1[n][i] * s2[m][j]) % mod;
}
}
for(int i = 0;i <= n;++i) sf[i][0] = 1;
for(int i = 0;i <= m;++i) sf[0][i] = 1;
for(int i = 1;i <= n;++i){
for(int j = 1;j <= m;++j){
sf[i][j] = sf[i - 1][j] + sf[i][j - 1];mo1(sf[i][j]);
sf[i][j] -= sf[i - 1][j - 1];mo2(sf[i][j]);
sg[i][j] = sg[i - 1][j] + sg[i][j - 1];mo1(sg[i][j]);
sg[i][j] -= sg[i - 1][j - 1];mo2(sg[i][j]);
if(a[i] == b[j]) sf[i][j] += get(sf,prea[i],preb[j],i - 1,j - 1);
if(a[i] > b[j]) sg[i][j] += get(sf,prea[i],preb[j],i - 1,j - 1);
mo1(sg[i][j]);mo1(sf[i][j]);
sg[i][j] += get(sg,prea[i],preb[j],i - 1,j - 1);
mo1(sg[i][j]);
}
}
ans += sg[n][m];
printf("%lld\n",ans % mod);
return 0;
}