动态规划之路

[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[i1][jk]+ik)

这个第 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][k1]+max(j+1j01))

( j ∈ [ 0 , j − 1 ) ) (j\in{[0,j-1)}) (j[0,j1))

#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)s1s2

求出最大的分值。

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)=(4abb)abbabab=4334=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[i1][j1]+2

s 1 [ i ] ! = s 2 [ j ] s1[i]!=s2[j] s1[i]!=s2[j] ,这时候从最长公共子序列的转移方程我们知道要从 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] f [ i ] [ j − 1 ] f[i][j-1] f[i][j1] 转移来。

从这两个转移来都会有 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[i1][j],f[i][j1])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[i1][j],f[i][j1])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。要求输出

  1. tree \text{tree} tree 的最高加分。

  2. 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 1n<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 n1 条边构成,也就是说这是一棵树,牛妹可以任意选择一个点为根,根的深度 d e p r o o t dep_{root} deproot 0 0 0,对于任意一个非根的点,我们将他到根节点路径上的第一个点称作他的父节点,例如 1 1 1 为根, 1 − 4 1-4 14 的;路径为 1 − 3 − 5 − 4 1-3-5-4 1354 时, 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 1n106

思路:

换根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]+(nsz[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(1ain) ,中间以空格分隔,分别表示每个同学的身高排名。

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[i1][j1]+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 cntk 个人,然后是从 p r e j = j − ( k − ( c n t − k ) ) prej=j-(k-(cnt-k)) prej=j(k(cntk)) 转移来的,如果这个 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[i1][prej]A[cnt]prej=j(k(cntk))

#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=1min(sz[d],k)dp[d][j]
如果不连该边,那么他们两就没有关系,方案数当然就是乘上 j j j 中选 1 − k 1-k 1k 个的总方案数。

心得:

一开始卡在状态设置上,因为树形 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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值