[SCOI2009]粉刷匠
——(两层dp) 难度:2
题目描述
windy有 N 条木板需要被粉刷。 每条木板被分为 M 个格子。 每个格子要被刷成红色或蓝色。
windy每次粉刷,只能选择一条木板上一段连续的格子,然后涂上一种颜色。 每个格子最多只能被粉刷一次。
如果windy只能粉刷 T 次,他最多能正确粉刷多少格子?
一个格子如果未被粉刷或者被粉刷错颜色,就算错误粉刷。
输入格式
第一行包含三个整数,N M T。
接下来有N行,每行一个长度为M的字符串,'0’表示红色,'1’表示蓝色。
输出格式
包含一个整数,最多能正确粉刷的格子数。
样例 #1
样例输入 #1
3 6 3
111111
000000
001100
样例输出 #1
16
提示
30%的数据,满足 1 <= N,M <= 10 ; 0 <= T <= 100 。
100%的数据,满足 1 <= N,M <= 50 ; 0 <= T <= 2500 。
思路:
设状态: g [ i ] [ j ] g[i][j] g[i][j] 表示前 i 个木板中刷 j 次所能获得的最大价值。
那么很容易得到转移方程: g [ i ] [ j ] = m a x ( g [ i − 1 ] [ j − k ] + 第 i 个 木 板 刷 了 k 次 的 最 大 价 值 ) g[i][j]=max(g[i-1][j-k]+第i个木板刷了k次的最大价值) g[i][j]=max(g[i−1][j−k]+第i个木板刷了k次的最大价值)
这个第 i 个木板刷了 k 次的最大价值怎么求呢?
由于 k 次怎么刷也涉及决策,所以再dp一次。
设状态: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k] 表示第 i 个模板前 j 个方块刷了 k 次的最大价值。
考虑第 k 次粉刷的情况:这一次粉刷可以是从 0 到 j-1 的任意一个位置刷一遍到 j,然后价值就是
刷的这一个区间的 0 的个数和 1 的个数中取大值。
用前缀和维护即可 s u m [ i ] [ j ] sum[i][j] sum[i][j]
那么转移方程就是: f [ i ] [ j ] [ k ] = m a x ( f [ i ] [ j ′ ] [ k − 1 ] + m a x ( j ′ + 1 到 j 中 0 的 个 数 , 1 的 个 数 ) ) f[i][j][k]=max(f[i][j'][k-1]+max(j'+1到j中0的个数,1的个数)) f[i][j][k]=max(f[i][j′][k−1]+max(j′+1到j中0的个数,1的个数))
( j ∈ [ 0 , j − 1 ) ) (j\in{[0,j-1)}) (j∈[0,j−1))
#include <bits/stdc++.h>
using namespace std;
int f[55][55][2510];
int g[55][2510];
int a[55][55];
int sum[55][55];
string s[55];
int main()
{
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n;i++)
{
cin >> s[i];
s[i] = " " + s[i];
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
a[i][j] = s[i][j] - '0';
sum[i][j] = sum[i ][j-1] + a[i][j];
//cout << sum[i][j] << ' ';
}
//cout << '\n';
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
for (int k1 = 1; k1 <= k; k1++)
{
for (int j1 = 0; j1 < j; j1++)
{
f[i][j][k1] = max(f[i][j][k1], f[i][j1][k1 - 1] + max(sum[i][j] - sum[i][j1], j - j1 - sum[i][j] + sum[i][j1]));
}
}
}
}
for (int i = 1; i <= n;i++)
{
for (int j = 0; j <= k;j++)
{
for (int k1 = 0; k1 <= j;k1++)
{
g[i][j] = max(g[i][j], g[i - 1][j - k1] + f[i][m][k1]);
}
}
}
printf("%d", g[n][k]);
}
(CF 683 Div1 B)Catching Cheaters
——(最长公共子序列思想) 难度:2
题目描述:
给定两个串 s 1 s1 s1, s 2 s2 s2,求出两个串的子串的最长公共子序列的分值,分值 = 4 × ∣ L C S ( s 1 ′ , s 2 ′ ) ∣ − ∣ s 1 ′ ∣ − ∣ s 2 ′ ∣ =4\times|LCS(s1',s2')|-|s1'|-|s2'| =4×∣LCS(s1′,s2′)∣−∣s1′∣−∣s2′∣
求出最大的分值。
Examples
input
4 5
abba
babab
output
5
For the first case:
a b b abb abb from the first string and abab from the second string have LCS equal to a b b abb abb.
The result is S ( a b b , a b a b ) = ( 4 ⋅ ∣ a b b ∣ ) − ∣ a b b ∣ − ∣ a b a b ∣ = 4 ⋅ 3 − 3 − 4 = 5. S(abb,abab)=(4⋅|abb|) - |abb| - |abab| = 4⋅3−3−4=5. S(abb,abab)=(4⋅∣abb∣)−∣abb∣−∣abab∣=4⋅3−3−4=5.
思路:
设 f [ i ] [ j ] f[i][j] f[i][j] 表示 s1 以 i 结尾,s2 以 j 结尾的子串的最大分值。以某某结尾就是子串的性质。
当 s 1 [ i ] = = s 2 [ j ] s1[i]==s2[j] s1[i]==s2[j] ,这时候 LCS的长度增加了 1,分值上增加 4,而两个子串 s1‘,s2’ 各增加 1,所以减去 2,整体增加 2.
则有 f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 2 f[i][j]=f[i-1][j-1]+2 f[i][j]=f[i−1][j−1]+2
当 s 1 [ i ] ! = s 2 [ j ] s1[i]!=s2[j] s1[i]!=s2[j] ,这时候从最长公共子序列的转移方程我们知道要从 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j] 或 f [ i ] [ j − 1 ] f[i][j-1] f[i][j−1] 转移来。
从这两个转移来都会有 LCS 长度不变,但是两个子串其中一个变长 1,所以整体 减一。
则有 f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) − 1 f[i][j]=max(f[i-1][j],f[i][j-1])-1 f[i][j]=max(f[i−1][j],f[i][j−1])−1 。但是,还有可能不从前面任何状态转移来,自己重新开始, f [ i ] [ j ] = 0 f[i][j]=0 f[i][j]=0。
所以 f [ i ] [ j ] = m a x ( m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) − 1 , 0 ) f[i][j]=max(max(f[i-1][j],f[i][j-1])-1,0) f[i][j]=max(max(f[i−1][j],f[i][j−1])−1,0)。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll f[5010][5010];
int main()
{
int n1, n2;
cin >> n1 >> n2;
string s1, s2;
cin >> s1 >> s2;
ll ans = 0;
for (int i = 1; i <= n1;i++)
{
for (int j = 1; j <= n2;j++)
{
if(s1[i-1]==s2[j-1])
f[i][j] = f[i - 1][j - 1] + 2;
else
f[i][j] = max(max(f[i][j - 1], f[i - 1][j]) - 1, 1LL*0);
ans = max(f[i][j], ans);
}
}
cout << ans;
}
[NOIP2003 提高组] 加分二叉树
——简单区间dp 难度:1
题目描述
设一个 n n n 个节点的二叉树 tree \text{tree} tree 的中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,…,n),其中数字 1 , 2 , 3 , … , n 1,2,3,\ldots,n 1,2,3,…,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i i i 个节点的分数为 d i d_i di, tree \text{tree} tree 及它的每个子树都有一个加分,任一棵子树 subtree \text{subtree} subtree(也包含 tree \text{tree} tree 本身)的加分计算方法如下:
subtree \text{subtree} subtree 的左子树的加分 × \times × subtree \text{subtree} subtree 的右子树的加分 + + + subtree \text{subtree} subtree 的根的分数。
若某个子树为空,规定其加分为 1 1 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,…,n) 且加分最高的二叉树 tree \text{tree} tree。要求输出
-
tree \text{tree} tree 的最高加分。
-
tree \text{tree} tree 的前序遍历。
输入格式
第 1 1 1 行 1 1 1 个整数 n n n,为节点个数。
第 2 2 2 行 n n n 个用空格隔开的整数,为每个节点的分数
输出格式
第 1 1 1 行 1 1 1 个整数,为最高加分($ Ans \le 4,000,000,000$)。
第 2 2 2 行 n n n 个用空格隔开的整数,为该树的前序遍历。
样例 #1
样例输入 #1
5
5 7 1 2 10
样例输出 #1
145
3 1 2 4 5
提示
数据规模与约定
对于全部的测试点,保证 1 ≤ n < 30 1 \leq n< 30 1≤n<30,节点的分数是小于 100 100 100 的正整数,答案不超过 4 × 1 0 9 4 \times 10^9 4×109。
比较简单就直接看代码了
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll dp[35][35];
int root[35][35];
ll a[35];
int n;
ll dfs(int l,int r)
{
if(l==r)
return a[l];
if(r<l)
return 1;
if(dp[l][r]!=-1)
return dp[l][r];
ll maxn = -1;
int maxi;
for (int i = l; i <= r;i++)
{
if (dfs(l, i - 1) * dfs(i + 1, r) + a[i]>maxn)
{
maxn = dfs(l, i - 1) * dfs(i + 1, r) + a[i];
maxi = i;
}
}
root[l][r] = maxi;
return dp[l][r] = maxn;
}
void dfs1(int l,int r)
{
if(l>r)
return;
else if(l==r)
cout << l << ' ';
else{
cout << root[l][r] << ' ';
dfs1(l, root[l][r] - 1);
dfs1(root[l][r] + 1, r);
}
}
int main()
{
memset(dp, -1, sizeof dp);
scanf("%d", &n);
for (int i = 1; i <= n;i++)
scanf("%d", &a[i]);
cout << dfs(1, n) << endl;
dfs1(1, n);
}
树学
——换根dp 难度:1
题目描述
牛妹有一张连通图,由 n n n 个点和 n − 1 n-1 n−1 条边构成,也就是说这是一棵树,牛妹可以任意选择一个点为根,根的深度 d e p r o o t dep_{root} deproot为 0 0 0,对于任意一个非根的点,我们将他到根节点路径上的第一个点称作他的父节点,例如 1 1 1 为根, 1 − 4 1-4 1−4 的;路径为 1 − 3 − 5 − 4 1-3-5-4 1−3−5−4 时, 4 4 4 的父节点是 5 5 5,并且满足对任意非根节点, d e p i = d e p f a i + 1 dep_i=dep_{fai}+1 depi=depfai+1,整棵树的价值 W = ∑ i = 1 n d e p i W=∑_{i=1}^{n}dep_i W=∑i=1ndepi,即所有点的深度和
牛妹希望这棵树的W最小,请你告诉她,选择哪个点可以使W最小
输入描述:
第一行,一个数,n
接下来n-1行,每行两个数x,y,代表x-y是树上的一条边
输出描述:
一行,一个数,最小的W
输入
[复制](javascript:void(0)😉
4
1 2
1 3
1 4
输出
[复制](javascript:void(0)😉
3
备注:
1 ≤ n ≤ 1 0 6 1≤n≤10^6 1≤n≤106
思路:
换根dp模板。
通过这道题可以知道:换根dp是自上而下换根,并且每次要换的那个节点都处于第二层,基于这个特点我们容易得到转移方程:
d p [ d ] = d p [ u ] + ( n − s z [ d ] ) − s z [ d ] dp[d]=dp[u]+(n-sz[d])-sz[d] dp[d]=dp[u]+(n−sz[d])−sz[d] 即除了 d d d 为子树的其他点深度都拔高了 1 1 1,而 d d d 子树自己的深度都减少 1 1 1。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000010;
int sz[maxn];
int f[maxn];
int dep[maxn];
vector<int> g[maxn];
int ans = 0x3f3f3f3f;
int n;
void dfs(int u, int fa, int deep)
{
sz[u] = 1;
dep[u] = deep;
f[u] = dep[u];
for (auto d : g[u])
{
if (d == fa)
continue;
dfs(d, u,deep+1);
sz[u] += sz[d];
f[u] += f[d];
}
}
void dfs1(int u,int fa)
{
for(auto d:g[u])
{
if(d==fa)
continue;
f[d] = f[u] + (n - sz[d]) - sz[d];
ans = min(ans, f[d]);
dfs1(d, u);
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1, -1, 0);
// for (int i = 1; i <= n;i++)
// cout << f[i] << ' ';
ans = f[1];
dfs1(1, -1);
cout << ans;
}
D. Zztrans 的班级合照
——计数类 难度:3
Zztrans 决定和他的同学们去拍一张班级合照,为了让每个人都能在照片里出镜,他们约定按如下方式站队进行拍照:排成人数相同的两排,每排从左向右身高都不递减,且第二排同学的身高不低于第一排对应位置同学的身高。
Zztrans 擅长排序,他得到了每个同学身高从低到高排序后的排名(身高相同的同学排名相同),这样他就不用一个个统计每个同学的身高了。但是他并不擅长数数,希望你教教他一共有多少种排队的方案。
由于答案可能很大,你只需要输出答案对 998244353 998244353 998244353 取模后的结果。
Input
第一行有一个整数 n n n $ (2≤n≤5000)$,表示 Zztrans 和他同学们的总人数,保证 n n n 为偶数。
第二行有 n n n 个整数 a 1 , a 2 , … , a n ( 1 ≤ a i ≤ n ) a_1,a_2,…,a_n (1≤a_i≤n) a1,a2,…,an(1≤ai≤n) ,中间以空格分隔,分别表示每个同学的身高排名。
Output
在一行输出一个整数,表示 Zztrans 和他的同学们按约定可能的排队方案数对 998244353 998244353 998244353 取模后的结果。
Examples
input
Copy
4
1 2 3 4
output
Copy
2
input
Copy
4
1 1 1 1
output
Copy
24
input
Copy
10
1 1 3 3 5 5 5 5 9 10
output
Copy
960
思路:
要让同学从左往右递增,且第一排同学对应位置不能高于第二排同学。
很容易想到就是对同学进行排序。排完序以后一个同学一个同学的放到这两排中。排序能保证后放的同学比先放的高从而满足从左往右递增的条件,那第二个条件怎么保证呢?
在放的过程第一排同学始终不少于第二排同学,直到最后一个同学才追平,但是多几个都有可能,所以这个条件要作为状态记录。
因此设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 个同学放完了,第一排比第二排多 j j j 个同学的方案数。
d p [ i ] [ j ] + = ( d p [ i − 1 ] [ j − 1 ] + d p [ i ] [ j + 1 ] ) dp[i][j]+=(dp[i-1][j-1]+dp[i][j+1]) dp[i][j]+=(dp[i−1][j−1]+dp[i][j+1]),第一个同学放第二排或第一个同学放第一排。
这个转移方程在相同身高人数都为 1 1 1 的情况下是对的,但是当身高相同的人有多个时要考虑这多个人是可以全排列的,也就是你从小到大排序的结果有很多个,那我们是不是排一次然后最后再把所有重复的阶乘乘起来就好呢?
显然不行,因为会有重复,比如你的第二个排序跟第一个排序就只在一处位置做了交换,你这两种排序之间会有很多一样的方案。
只能在 d p dp dp 的过程中处理这个问题了,我们把放一个同学改成放身高相同的人,记下每个身高有多少人,每放一个身高要乘以他人数的全排列,因为这 c n t cnt cnt 个人可以先排完序之后再分配给两排。假设每一次分配第一排给 k k k 个,那么第二排就给 c n t − k cnt-k cnt−k 个人,然后是从 p r e j = j − ( k − ( c n t − k ) ) prej=j-(k-(cnt-k)) prej=j−(k−(cnt−k)) 转移来的,如果这个 p r e j < 0 prej<0 prej<0 则说明第二排人数大于第一排直接continue。
方程: d p [ i ] [ j ] + = d p [ i − 1 ] [ p r e j ] ∗ A [ c n t ] , p r e j = j − ( k − ( c n t − k ) ) dp[i][j]+=dp[i-1][prej]*A[cnt],prej=j-(k-(cnt-k)) dp[i][j]+=dp[i−1][prej]∗A[cnt],prej=j−(k−(cnt−k))
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 5010;
const ll mod = 998244353;
int a[maxn];
int n;
int vis[maxn];
ll f[maxn][maxn];
ll A[maxn];
void ini()
{
A[1] = 1;
for (int i = 2; i <= 5e3; i++)
{
A[i] = (A[i - 1] * i) % mod;
}
}
int main()
{
ini();
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
vis[a[i]]++;
}
sort(a + 1, a + n + 1);
// reverse(a + 1, a + 1 + n);
int tot = unique(a + 1, a + n + 1) - a - 1;
f[0][0] = 1;
for (int i = 1; i <= tot; i++)
{
int num = vis[a[i]];
for (int j = 0; j <= n; j++)
{
for (int k = 0; k <= num; k++)
{
int prej = j + num - 2 * k;
if (prej < 0)
break;
(f[i][j] += (f[i - 1][prej] * A[num] % mod) % mod)%=mod;
}
}
}
printf("%lld\n", f[tot][0]);
}
蓝魔法师
——树上计数dp 难度:2
题目描述
“你,你认错人了。我真的,真的不是食人魔。”–蓝魔法师
给出一棵树,求有多少种删边方案,使得删后的图每个连通块大小小于等于k,两种方案不同当且仅当存在一条边在一个方案中被删除,而在另一个方案中未被删除,答案对998244353取模
输入描述:
第一行两个整数n,k, 表示点数和限制
2 <= n <= 2000, 1 <= k <= 2000
接下来n-1行,每行包括两个整数u,v,表示u,v两点之间有一条无向边
保证初始图联通且合法
输出描述:
共一行,一个整数表示方案数对998244353取模的结果
输入
[复制](javascript:void(0)😉
5 2
1 2
1 3
2 4
2 5
输出
[复制](javascript:void(0)😉
7
思路:
连通这个信息想来想去都不知道怎么维护,看了题解是:
设 d p [ u ] [ j ] dp[u][j] dp[u][j] 表示 u u u 节点在大小为 j j j 的连通块里的方案数。
最后答案是 ∑ i = 1 k d p [ 1 ] [ i ] \sum_{i=1}^{k} dp[1][i] ∑i=1kdp[1][i]
转移方程:既然是删边操作,那么选择当然是删不删这条边,即对于 u 与儿子 d 的边 e,是否连上。
用两个数组分别存连上和不连上的 : t m p 1 [ u ] [ i ] , t m p 2 [ u ] [ i ] tmp1[u][i],tmp2[u][i] tmp1[u][i],tmp2[u][i]
(一):首先是考虑连上
t
m
p
1
[
i
+
j
]
=
d
p
[
u
]
[
i
]
∗
d
p
[
d
]
[
j
]
tmp1[i+j]=dp[u][i]*dp[d][j]
tmp1[i+j]=dp[u][i]∗dp[d][j]
表示在
u
u
u 考虑过的子树中选
i
i
i 个点与
u
u
u 连通,没考虑过的就相当于选
0
0
0 个点连通,然后在正在考虑的当前子树
d
d
d 中选
j
j
j 个点与
u
u
u 连通,所以
u
u
u 就处于
i
+
j
i+j
i+j 大小的连通块中。(这里有点像背包)
(二):不连上
t
m
p
2
[
i
]
=
d
p
[
u
]
[
i
]
∗
∑
j
=
1
m
i
n
(
s
z
[
d
]
,
k
)
d
p
[
d
]
[
j
]
tmp2[i]=dp[u][i]*\sum_{j=1}^{min(sz[d],k)}dp[d][j]
tmp2[i]=dp[u][i]∗j=1∑min(sz[d],k)dp[d][j]
如果不连该边,那么他们两就没有关系,方案数当然就是乘上
j
j
j 中选
1
−
k
1-k
1−k 个的总方案数。
心得:
一开始卡在状态设置上,因为树形 d p dp dp 很多状态都是设为在以 u u u 为跟的子树怎么样怎么样,很少设 u u u 怎么样怎么样,所以算是一种没写过的题型。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define IOS \
ios::sync_with_stdio(0); \
cin.tie(0); \
cout.tie(0)
const int maxn = 2010;
const ll mod = 998244353;
int n, k;
ll dp[maxn][maxn];
int sz[maxn];
vector<int> g[maxn];
void dfs(int u, int fa)
{
sz[u] = 1;
dp[u][1] = 1;//对叶子的初始化
for (auto d : g[u])
{
if (d == fa)
continue;
dfs(d, u);
static ll tmp[maxn];//连该边方案数
static ll tmp1[maxn];//不连该边方案数
ll sum = 0;
for (int i = 1; i <= min(sz[d], k); i++)
(sum += dp[d][i]) %= mod;
//不连该边
for (int i = 1; i <= min(sz[u], k); i++)
tmp1[i] = (dp[u][i] * sum) % mod;
//连上该边
for (int i = 1; i <= min(sz[u], k); i++)
{
for (int j = 1; i + j <= k && i + j <= sz[u] + sz[d] && j <= sz[d]; j++)
{
(tmp[i + j] += (dp[u][i] * dp[d][j]) % mod) %= mod;
}
//dp[u][i] = (dp[u][i] * sum) % mod;
}
sz[u] += sz[d];
for (int i = 1; i <= min(sz[u], k); i++)
{
dp[u][i] = (tmp[i]+tmp1[i])%mod;
tmp[i] = tmp1[i] = 0;
}
}
}
int main()
{
IOS;
cin >> n >> k;
for (int i = 1; i < n; i++)
{
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1, -1);
ll ans = 0;
for (int i = 1; i <= k; i++)
(ans += dp[1][i]) %= mod;
cout << ans << endl;
}