大三第四周学习笔记

周一

最近补一补icpc网络赛的题目

C Delete the Tree

签到题,但是当时卡了一下

首先每次操作都是删一个点,那么要删完肯定是n次操作。

所以就是让删除操作尽可能少。

可以发现,度数为1的点不能执行2操作,必须执行1操作,所以度数为1的点的个数是答案的下界。

那么有达到这个下界的做法呢?度数为1的点只有两种情况,一种是叶子,一种是根节点然后它只有一个儿子。

我们可以从叶子开始,删叶子使得叶子的父亲度数为2,然后执行2操作把父亲删掉,然后叶子就有新的父亲,然后继续删叶子使得父亲度数为2,一直重复,这样可以删完所有的点。

一开始WA了一发,后面注意到数据范围还有n=1的情况,这时度数为0。

以后写题时要注意数据范围里面n特别小的情况,很可能要特判

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int d[N], n;

int main()
{ 
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) d[i] = 0;
        _for(i, 1, n - 1)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            d[u]++; d[v]++;
        }

        int ans = 0;
        _for(i, 1, n)
            if(d[i] <= 1)
                ans++;
        printf("%d\n", ans);
    }

    return 0;
}

D Find the Number(打表)

当时就想着打表,可以算出最大是C(20, 10),当时把阶乘写出来感觉很大没去算,其实算一下挺小的。所以可以直接爆搜打表。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

set<int> s;

void dfs(int x, int num, int pos)
{
    if(num == 0)
    {
        if(x <= 1e9) s.insert(x);
        return;
    }
    if(pos == 30) return;

    dfs(x | 1 << pos, num - 1, pos + 1);
    dfs(x, num, pos + 1);
}

int main()
{ 
    _for(i, 1, 16)
        dfs(1 << i, i - 1, i + 1);
    
    int T; scanf("%d", &T);
    while(T--)
    {
        int l, r;
        scanf("%d%d", &l, &r);

        auto it = s.lower_bound(l);
        if(it != s.end() && *it <= r) printf("%d\n", *it);
        else puts("-1");
    }

    return 0;
}

G Read the Documentation(dp)

还是需要多练习dp,dp在比赛中非常常见

充分利用n小,最长只有4段的条件

用dp[i][p1][p2][p3][p4]状态的最优解,其实想到这个状态下面就很简单了

限制一下p1 p2 p3 p4的上界,然后由于最多往前i-5,i到i-5有6个,所以滚动数组将下标模6

然后因为中间要空一格,所以前一个状态可能到-1,而-1却是合法的,这时要处理一下。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 110;
ll dp[6][55][35][27][22], k[5], x[5], s[N];
int n, T;

int main()
{ 
    scanf("%d%d", &n, &T);
    _for(i, 1, 4)
    {
        scanf("%lld", &x[i]);
        k[i] = k[i - 1] + x[i];
    }
    _for(i, 1, n)
    {
        ll x; scanf("%lld", &x);
        s[i] = s[i - 1] + x;
    }

    ll ans = 0;
    _for(i, 1, n)
        _for(p1, 0, n / 2 + 1)
             _for(p2, 0, n / 3 + 1)
                _for(p3, 0, n / 4 + 1)
                    _for(p4, 0, n / 5 + 1)
                    {
                        if(p1 * k[1] + p2 * k[2] + p3 * k[3] + p4 * k[4] > T) break;

                        ll res = dp[(i - 1 + 6) % 6][p1][p2][p3][p4];
                        if(p1 - 1 >= 0 && i - 2 >= -1) res = max(res, dp[(i - 2 + 6) % 6][p1 - 1][p2][p3][p4] + s[i] - s[i - 1]);
                        if(p2 - 1 >= 0 && i - 3 >= -1) res = max(res, dp[(i - 3 + 6) % 6][p1][p2 - 1][p3][p4] + s[i] - s[i - 2]);
                        if(p3 - 1 >= 0 && i - 4 >= -1) res = max(res, dp[(i - 4 + 6) % 6][p1][p2][p3 - 1][p4] + s[i] - s[i - 3]);
                        if(p4 - 1 >= 0 && i - 5 >= -1) res = max(res, dp[(i - 5 + 6) % 6][p1][p2][p3][p4 - 1] + s[i] - s[i - 4]);

                        dp[i % 6][p1][p2][p3][p4] = res;
                        if(i == n) ans = max(ans, dp[i % 6][p1][p2][p3][p4]);
                    }
    printf("%lld\n", ans);

    return 0;
}

L LCS-like Problem(dp)

这题当时我想到从t中处理一个ban[][]数组,然后再s中dp找一个最长的不会被ban的序列

我卡住点的在于,如果当前要选一个字符,那么显然要和之前选过的所有字符看会不会被ban,那么是很费时间的。

其实,只需要判断最近的那个是否被ban,如果最近的那个不会被ban,那么和前面所有的都不会被ban

因为考虑s和t匹配,如果s中i<j,那么在t中对应的位置一定是i>j  那么就有在t中的子序列恰好是s中反过来的。

那么新来一个字符,如果它不和最近的冲突,那么它在t中匹配一定在这个最近的之前,也就在所有的之前。

知道这一点后就很好处理了。

首先可能出现s中的字符t中没有匹配的情况,这种时候这些字符是一定选的,处理一下就好。

那么用dp[j]表示以字符j结尾的最长序列,那么每次对当前的s[i],枚举j,看能不能加上即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int dp[30], ban[30][30], vis[30], ans;

int main()
{ 
    string s, t;
    cin >> s >> t;

    int len = t.size();
    for(int i = len - 1; i >= 0; i--)
    {
        _for(j, 0, 25)
            if(vis[j])
                ban[t[i] - 'a'][j] = 1;
        vis[t[i] - 'a'] = 1;
    }

    for(auto x: s)
    {
        if(!vis[x - 'a']) ans++;
        else 
        {
            int cur = 0;
            _for(j, 0, 25)
                if(!ban[j][x - 'a']) 
                    cur = max(cur, dp[j] + 1);
            dp[x - 'a'] = max(dp[x - 'a'], cur);
        }
    }

    int mx = 0;
    _for(i, 0, 25) mx = max(mx, dp[i]);
    printf("%d\n", mx + ans);

    return 0;
}

J Gachapon(构造)

从样例猜构造方法,可惜猜的是最低为1/2  其实把最低的换成n就对了

这个分数加减比较复杂,考虑容易约分的形式

也就是1/ 2  *2/3  * 3/4……

即(1-1/2)(1-1/3)……

所以可以考虑构造这种分数

设初始为A,然后1/m到1/n

然后一波激情运算,注意一些算式可以表示为一个变量,这样推起来方便,因为最终目的是为了写程序。

注意到变量只有m和A,于是可以解出m和A关系

最后枚举m算出A,看A是否满足条件即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
ll x, y;

ll gcd(ll a, ll b) { return !b ? a : gcd(b, a % b); }

int main()
{ 
    scanf("%lld%lld", &x, &y);

    ll sum = 0;
    _for(i, 2, x - 1) sum += i;

    _for(m, x - 1, 1e9)
    {
        ll n = m - x + 3;
        ll t = sum + (n - 1) * x;

        ll up = m * y - t;
        ll down = m - t;

        if(up < 0 && down < 0) up = -up, down = -down;
        if(up * down <= 0 || up >= down) continue;
        ll d = gcd(up, down);
        up /= d;
        down /= d;

        if(down > 10000) continue;

        printf("%lld %lld\n", up, down);
        for(ll i = m; i >= n; i--) printf("1 %lld\n", i);
        puts("1 1");
        break;
    }

    return 0;
}

抽卡(概率)

入门题,只需要算一下不可能的概率,然后用1减去就行

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int mod = 1e9 + 7;
const int N = 1e5 + 10;
int a[N], b[N], n;

ll binpow(ll a, ll b)
{
    ll res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = res * a % mod;
        a = a * a % mod;
    }
    return res;
}

ll inv(ll x) { return binpow(x, mod - 2); }

int main()
{ 
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &a[i]);
    _for(i, 1, n) scanf("%d", &b[i]);

    ll ans = 1;
    _for(i, 1, n)
        ans = ans * (a[i] - b[i]) % mod * inv(a[i]) % mod;
    printf("%lld\n", (1 - ans + mod) % mod);

    return 0;
}

周二

一袋小球(概率dp)

讨论一下什么情况先手能赢即可。

注意边界条件

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
double dp[N][N];
int A, B;

int main()
{ 
    scanf("%d%d", &A, &B);
    _for(i, 1, A) dp[i][0] = 1;
    
    _for(a, 1, A)
        _for(b, 1, B)
        {
            dp[a][b] = 1.0 * a / (a + b);
            if(b >= 3) dp[a][b] += 1.0 * b / (a + b) * (b - 1) * (b - 2)
                    / (a + b - 1) / (a + b - 2) * dp[a][b - 3];
            if(a >= 1 && b >= 2)
                dp[a][b] += 1.0 * b / (a + b) * (b - 1) / (a + b - 1) * a
                            / (a + b - 2) * dp[a - 1][b - 2];
        }
    printf("%.10f\n", dp[A][B]);

    return 0;
}

P2473 [SCOI2008] 奖励关(状压dp)

记住概率顺推,期望逆推。

首先n很小,马上想到状压dp

如果是正常的思路,那么就是对于当前S,枚举它的子集转移过来

但是这题要算期望,怎么处理呢?这题有一些条件限制,满足si才能买,这使得概率并不均匀,考虑起来很复杂

因此反过来处理,也就是逆推,就很好处理

在当前这个状态下,抽到哪一个东西的概率是均等的,所以就加上n种情况的结果,然后除以n即可。

做期望的时候,逆推的思考方式是很方便的。

此外,这题我开始在想最优策略是什么,其实意思就是dp取最大值,这样就是最优策略

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int p[20], sta[20], n, k;
double dp[110][1 << 15];

int main()
{ 
    scanf("%d%d", &k, &n);
    rep(i, 0, n)
    {
        scanf("%d", &p[i]);
        int x;
        while(scanf("%d", &x) && x) 
            sta[i] |= 1 << (x - 1);
    }

    for(int i = k; i >= 1; i--)
        rep(S, 0, 1 << n)
        {
            rep(j, 0, n)
            {
                if((S & sta[j]) == sta[j]) dp[i][S] += max(dp[i + 1][S], dp[i + 1][S | 1 << j] + p[j]);  //注意包含的写法
                else dp[i][S] += dp[i + 1][S];
            }
            dp[i][S] /= n;
        }
    printf("%.6f\n", dp[1][0]);   //有些题目固定说保留几位小数,要注意

    return 0;
}

Accumulation Degree(换根dp+细节)

这题的细节搞了我好久

首先对于流量,按照题目的从根到各个叶子,流量会分叉比较难思考,所以我们反过来,看作从叶子到根的流量

用dp[i]表示以i为根的子树的最大流量

那么dp[u] = sigma min(w, dp[v])

特例是如果u是叶子的话,dp[u] = 0同时v是叶子的话,取w而不是min(w, dp[v])

所以儿子v的贡献是需要分类讨论的,而当换根的时候也会出现这种情况,v可能是儿子,u可能变成儿子,如果它们成儿子时是叶子的话,贡献就不一样。

所以干脆写一个函数min(w, v == 0 ? 1e9 : v) 来计算贡献

这样就非常清晰了,不用考虑哪些度数为1的情况

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
vector<pair<int, int>> g[N];
int dp[N], f[N], n;

int val(int w, int v)
{
    return min(w, v == 0 ? (int)1e9 : v);
}

void dfs(int u, int fa)
{
    dp[u] = 0;
    for(auto x: g[u])
    {
        int v = x.first, w = x.second;
        if(v == fa) continue;
        dfs(v, u);
        dp[u] += val(w, dp[v]);
    }
}

void dfs2(int u, int fa)
{
    for(auto x: g[u])
    {
        int v = x.first, w = x.second;
        if(v == fa) continue;
        f[v] = dp[v] + val(w, f[u] - val(w, dp[v]));
        dfs2(v, u);
    }
}

int main()
{ 
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) g[i].clear();
        _for(i, 1, n - 1)
        {
            int u, v, w;
            scanf("%d%d%d", &u, &v, &w);
            g[u].push_back({v, w});
            g[v].push_back({u, w});
        }

        dfs(1, 0);
        f[1] = dp[1];
        dfs2(1, 0);

        int res = 0;
        _for(i, 1, n) res = max(res, f[i]);
        printf("%d\n", res);
    }

    return 0;
}

周三

[NOIP2017]宝藏(状压dp)

好题啊

n这么小肯定是状压或者搜索,要马上反应过来

这题的难点在于怎么处理这个生成树

发现价值的计算只与层数有关,所以我们并不关心具体的构造,只需要关心层数

那么用dp[S][i]表示当前生成树已选的点为S,有i层的最小花费

要转移的话,就枚举第i层选哪些点,这些点的花费全部算为乘上i

注意,这样虽然会把答案算偏大,但是一定包括了正确答案

那么转移方程就是dp[S][i] = min(dp[S0][i - 1] + cost(S0, S))

S0是S的子集,枚举状态,再枚举它的子集,时间复杂度是3^n,不是4^n

考虑这里的cost(S0,S)该怎么算,显然可以枚举多出来的点向S0中的点连边

这个东西可以预处理,不写在循环中而加快速度。

预处理是3 ^ n * n ^ 2  dp是3 ^ n * n 花费主要在预处理

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int n, m, g[20][20], f[1 << 12][1 << 12], dp[1 << 12][20];

int main()
{ 
    memset(g, 0x3f, sizeof g);
    scanf("%d%d", &n, &m);
    while(m--)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        u--; v--;
        g[u][v] = g[v][u] = min(g[u][v], w);
    }

    memset(f, 0x3f, sizeof f);
    rep(S, 0, 1 << n)
        for(int S0 = S; S0; S0 = (S0 - 1) & S)
        {
            int res = 0;
            rep(j, 0, n)
                if((S & (1 << j)) && !(S0 & (1 << j)))
                {
                    int cur = 1e9;
                    rep(k, 0, n)
                        if(S0 & (1 << k))
                            cur = min(cur, g[j][k]);
                    if(cur == 1e9) 
                    {
                        res = 1e9;
                        break;
                    }
                    else res += cur;
                }
            f[S0][S] = res;
        }

    memset(dp, 0x3f, sizeof dp);
    rep(i, 0, n) dp[1 << i][0] = 0;
    rep(i, 1, n - 1)
        rep(S, 0, 1 << n)
            for(int S0 = S; S0; S0 = (S0 - 1) & S)
                if(f[S0][S] < 1e9)
                    dp[S][i] = min(dp[S][i], dp[S0][i - 1] + f[S0][S] * i);
    
    int ans = 1e9;
    _for(i, 0, n - 1) ans = min(ans, dp[(1 << n) - 1][i]);
    printf("%d\n", ans);

    return 0;
}

刷野(区间dp)

从数据范围可以看出是区间dp

但是这个区间dp有点技巧

在一个区间里面选一个野怪时,它的左右边是谁呢?

不一定,所以我们就把左边的杀完,右边的杀完,在杀当前这个,这样它的左边是l-1,右边是r+1

人为规定了这个顺序后,就可以计算了,而且这个规定也没有很特殊,可以包含所有情况

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 500 + 10;
int dp[N][N], a[N], b[N], n;

int main()
{ 
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &a[i]);
    _for(i, 1, n) scanf("%d", &b[i]);
    
    memset(dp, 0x3f, sizeof dp);
    _for(i, 1, n) dp[i][i] = a[i] + b[i - 1] + b[i + 1];
    _for(len, 2, n)
        _for(l, 1, n)
        {
            int r = l + len - 1;
            if(r > n) break;
            _for(k, l + 1, r - 1)
                dp[l][r] = min(dp[l][r], dp[l][k - 1] + dp[k + 1][r] + a[k] + b[l - 1] + b[r + 1]);
            dp[l][r] = min(dp[l][r], a[l] + dp[l + 1][r] + b[l - 1] + b[r + 1]);
            dp[l][r] = min(dp[l][r], a[r] + dp[l][r - 1] + b[l - 1] + b[r + 1]);
        }
    printf("%d\n", dp[1][n]);

    return 0;
}

[HAOI2011]PROBLEM A(dp)

首先转化一下,求最多右多少人说了真话,这样比较容易处理。

首先可以转化成排名区间,我一开始想的是,只要两个区间有交集就是冲突的,所以直接按照右端点排序贪心选取即可,然后就WA了

关键点在于一个特例,如果右多个相同的区间,且它们长度大于1,那么根据这道题,就是合法的!我就是这点没考虑到

考虑到这点后,就给区间附上了权值,且区间权值的最大值就是区间长度。

那么问题就变成了每个区间有权值,选不相交的区间使得权值最大

这个可以dp解决,dp[i]表示1~i的最优值,然后枚举右端点为i的区间转移即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
map<pair<int, int>, int> mp;
vector<pair<int, int>> g[N];
int dp[N], n, ans;

int main()
{ 
    scanf("%d", &n);
    _for(i, 1, n)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        if(a + b < n) mp[{b + 1, n - a}]++;
    }
    for(auto x: mp)
    {
        int l = x.first.first, r = x.first.second, v = x.second;
        g[r].push_back({l, min(r - l + 1, v)});
    }

    _for(i, 1, n)
    {
        dp[i] = dp[i - 1];
        for(auto x: g[i])
        {
            int j = x.first, v = x.second; 
            dp[i] = max(dp[i], v + dp[j - 1]);
        }
    }
    printf("%d\n", n - dp[n]);

    return 0;
}

Min酱要旅行(背包拓展)

逆向思考,不取i的,那就把一定取i的去掉

那么问题就转为如何求体积为j,一定取i的方案数

那么就是j-w[i]后,一定不取i的方案数

那么干脆就把g[j]表示为体积为j,一定不取i的方案数

有g[j] = f[j] - g[j - w[i]]

因此求出f之后,递推求出g数组即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2500;
int f[N], g[N], w[N], n, m;

int main()
{ 
    scanf("%d%d", &n, &m);
    _for(i, 1, n) scanf("%d", &w[i]);

    f[0] = 1;
    _for(i, 1, n)
        for(int j = m; j >= w[i]; j--)
            f[j] = (f[j] + f[j - w[i]]) % 10;
    
    _for(i, 1, n)
    {
        _for(j, 0, m)  //注意体积从0开始枚举
        {
            g[j] = f[j];
            if(j - w[i] >= 0) g[j] = (g[j] - g[j - w[i]] + 10) % 10;
        }
        _for(j, 1, m) printf("%d", g[j]);
        puts("");
    }

    return 0;
}

周四

管道取珠(思维+dp)

这题的关键在于平方和

考虑其物理意义

即两个一模一样的装置,结果相同的方案数

比如一个装置中某个方案为ai种,另一个相同的装置此方案也是ai

那么相同的就是ai^2

那么就根据这个dp

dp[i][j][k][l]表示第一个装置上面出了i个,下面出了j个,第二个装置上面k个下面l个时相同的方案数

那么如果a[i] == a[k] dp[i][j][k][l] += dp[i - 1][j][k - 1][l]

这样是n的四次方的

但是发现l是多余的,因为i+j=k+l

所以可以优化掉一个维度

初始化为dp[0][0][0] = 1  转移时从0开始枚举,注意负数情况 

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 500 + 10;
const int mod = 1024523;
int dp[N][N][N], n, m;
char a[N], b[N];

int main()
{ 
    scanf("%d%d%s%s", &n, &m, a + 1, b + 1);
    reverse(a + 1, a + n + 1);
    reverse(b + 1, b + m + 1);
    
    dp[0][0][0] = 1;
    _for(i, 0, n)
        _for(j, 0, m)
            _for(k, 0, n)
            {
                int l = i + j - k;
                if(i && k && a[i] == a[k]) dp[i][j][k] += dp[i - 1][j][k - 1];
                if(i && l && a[i] == b[l]) dp[i][j][k] += dp[i - 1][j][k];
                if(j && k && b[j] == a[k]) dp[i][j][k] += dp[i][j - 1][k - 1];
                if(j && l && b[j] == b[l]) dp[i][j][k] += dp[i][j - 1][k];
                dp[i][j][k] %= mod;
            }
    printf("%d\n", dp[n][m][n]);

    return 0;
}

周五

打砖块(观察+dp)

这题的关键点在于,看成一列一列的放,同时下一列的高度要大于等于当前这一列减去一

发现无论怎么选都有这一个规律

根据这个dp即可,一列一列来

注意有一列什么都不放的情况

最后统计答案要统计最后一列,因为中间有一些不合法的情况

也可也从第n列从第1列,这样每个状态都是合法的

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 50 + 10;
const int M = 500 + 10;
int dp[N][N][M], a[N][N], sum[N][N], n, m;

int main()
{ 
    scanf("%d%d", &n, &m);
    _for(i, 1, n)
        _for(j, 1, n - i + 1)
            scanf("%d", &a[i][j]);

    _for(j, 1, n)
        _for(i, 1, n - j + 1)
            sum[j][i] = sum[j][i - 1] + a[i][j];
   
    memset(dp, -0x3f, sizeof dp);
    _for(i, 0, n) dp[1][i][i] = sum[1][i];
    _for(j, 1, n)
        _for(i, 0, n - j + 1)
            _for(l, max(i - 1, 0), n - j)
                _for(k, 0, m - l)
                    dp[j + 1][l][k + l] = max(dp[j + 1][l][k + l], dp[j][i][k] + sum[j + 1][l]);
    printf("%d\n", max(dp[n][0][m], dp[n][1][m]));
                        
    return 0;
}

周六

[HAOI2010]最长公共子序列(dp+方案数)

这题有很多点可以学习

用g[i][j]表示前i个和前j个的最长公共子序列的个数

可以先用dp转移完,然后再统计方案数

由哪些状态可以转移过来,就转移哪些状态的方案数

但是有一个特例,当dp[i][j] == dp[i - 1][j - 1]时,多的a[i]和b[j]并没有产生贡献

这时g[i - 1][j - 1]给了g[i][j - 1],再给g[i][j],同时g[i - 1][j - 1]给了g[i - 1][j] 再给g[i][j]

重复计算了,所以要减去g[i - 1][j - 1]

此外,此题限制空间,需要滚动数组。

滚动数组的时候要记得当前的数组是已经有值的,要处理一下,要么覆盖要么初始化为0,也就是清空。

g数组的初始化为长度为0的最长公共子序列的方案数为1。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 5000 + 10;
const int mod = 1e8;
char a[N], b[N];
int dp[2][N], g[2][N];

int main()
{ 
    scanf("%s%s", a + 1, b + 1);
    int lena = strlen(a + 1) - 1;
    int lenb = strlen(b + 1) - 1;

    g[1][0] = 1;
    _for(i, 0, lenb) g[0][i] = 1;
    _for(i, 1, lena)
    {
        int cur = i % 2, pre = (i - 1) % 2;
        _for(j, 1, lenb)
        {
            dp[cur][j] = max(dp[pre][j], dp[cur][j - 1]);
            if(a[i] == b[j]) dp[cur][j] = max(dp[cur][j], dp[pre][j - 1] + 1);
            g[cur][j] = 0;

            if(a[i] == b[j] && dp[pre][j - 1] + 1 == dp[cur][j]) g[cur][j] += g[pre][j - 1];
            if(dp[cur][j] == dp[pre][j]) g[cur][j] += g[pre][j];
            if(dp[cur][j] == dp[cur][j - 1]) g[cur][j] += g[cur][j - 1];
            if(dp[cur][j] == dp[pre][j - 1]) g[cur][j] -= g[pre][j - 1];
            
            g[cur][j] = (g[cur][j] + mod) % mod;
        }
    }
    printf("%d\n%d\n", dp[lena % 2][lenb], g[lena % 2][lenb]);
                        
    return 0;
}

[AHOI2009]CHESS 中国象棋(dp求方案数)

数据比较小的时候可以用三进制状压做,和之前做一个八皇后方案数是类似的

但是题目给的是100,就无法状压了

考虑简化状态,实际上只需要考虑有多少列放了1个,多少放了0个,多少放了2个即可

用dp[i][j][k]表示前i行,有j列放了0个,k列放了1个

对于放了2个即m-j-k

那么枚举当前行怎么放即可

为了思考的方便,可以写成往后推的dp。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int mod = 9999973;
const int N = 110;
ll dp[N][N][N];
int n, m;

int main()
{ 
    scanf("%d%d", &n, &m);
    dp[1][m][0] = 1;
    dp[1][m - 1][1] = m;
    dp[1][m - 2][2] = m * (m - 1) / 2;

    _for(i, 1, n - 1)
		_for(j, 0, m)     // 0
			_for(k, 0, m)  // 1
			{
				int l = m - j - k; // 2
				if(l < 0) break;

				//一个都不放
				dp[i + 1][j][k] = (dp[i + 1][j][k] + dp[i][j][k]) % mod;

				//放一个
				//放在0列
				if(j > 0) dp[i + 1][j - 1][k + 1] = (dp[i + 1][j - 1][k + 1] + dp[i][j][k] * j % mod) % mod;
				//放在1列
				if(k > 0) dp[i + 1][j][k - 1] = (dp[i + 1][j][k - 1] + dp[i][j][k] * k % mod) % mod;

				//放两个
				//1个0列 1个0列
				if(j > 1) dp[i + 1][j - 2][k + 2] = (dp[i + 1][j - 2][k + 2] + dp[i][j][k] * (j * (j - 1) / 2) % mod) % mod;
				//1个1列 1个0列
				if(j > 0 && k > 0) dp[i + 1][j - 1][k] = (dp[i + 1][j - 1][k] + dp[i][j][k] * j * k % mod) % mod;
				//1个1列 1个1列
				if(k > 1) dp[i + 1][j][k - 2] = (dp[i + 1][j][k - 2] + dp[i][j][k] * (k * (k - 1) / 2) % mod) % mod;
			}
	
	ll ans = 0;
	_for(j, 0, m)    
		_for(k, 0, m)  
		{
			int l = m - j - k; 
			if(l < 0) break;
			ans = (ans + dp[n][j][k]) % mod;
		}
	printf("%lld\n", ans);
    
    return 0;
}

[SCOI2009]粉刷匠(dp)

读完题就知道是一个分组背包,关键是怎么求一行。

我一开始卡住在于觉得涂的时候只是涂一部分,其实可以发现把全部涂满的方案一定是最优方案之一,因为不涂白不涂,有了这一点就容易dp了

f[i][j]表示用j次把前i块涂满的最多正确的值,每次枚举最后一次涂多少就可以。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 60;
const int M = 2500 + 10;
int f[N][N], a[N], s[N], dp[M], n, m, T;
vector<pair<int, int>> ve[N];

void deal(int x)
{
	memset(f, 0, sizeof f);
	_for(i, 1, m)
		_for(j, 1, i)
			_for(k, 1, i)
				f[i][j] = max(f[i][j], f[k - 1][j - 1] + max(s[i] - s[k - 1], (i - k + 1) - (s[i] - s[k - 1])));
	_for(j, 1, m) ve[x].push_back({j, f[m][j]});
}

int main()
{ 
    scanf("%d%d%d", &n, &m, &T);
	_for(i, 1, n)
	{
		_for(j, 1, m) scanf("%1d", &a[j]), s[j] = s[j - 1] + a[j];
		deal(i);
	}
	
	_for(i, 1, n)
		for(int j = T; j >= 0; j--)
			for(auto x: ve[i])
			{
				int w = x.first, v = x.second;
				if(j >= w) dp[j] = max(dp[j], dp[j - w] + v);
			}
	printf("%d\n", dp[T]);

    return 0;
}

区间价值(dp)

用f[i]表示区间长度为i的答案

观察怎么转移 首先最后一个会消失,所以要求一个后缀不同数字的个数,可以O(n)求

然后发现,对于一个区间增加,要贡献1就要使得区间中没有这个数

转化一下,也就是这个数和前一个相同的差要大于等于i

那么我们可以求每个数与前面的数的差等于i的值,然后求一个后缀和使得大于等于i

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e6 + 10;
int a[N], vis[N], dif[N], pre[N], k[N], cnt, n;
ll f[N];

int main()
{ 
	scanf("%d", &n);
	_for(i, 1, n) scanf("%d", &a[i]);

	for(int i = n; i >= 1; i--)
	{
		if(!vis[a[i]])
		{
			vis[a[i]] = 1;
			cnt++;
		}
		dif[n - i + 1] = cnt;
	}

	_for(i, 1, n)
	{
		k[i - pre[a[i]]]++;
		pre[a[i]] = i;
	}
	for(int i = n - 1; i >= 1; i--) k[i] += k[i + 1];

	f[1] = n;
	_for(i, 2, n) f[i] = f[i - 1] + k[i] - dif[i - 1];
	
	int q; scanf("%d", &q);
	_for(i, 1, q)
	{
		int x; scanf("%d", &x);
		printf("%lld\n", f[x]);
	}

    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值