striving & singing

2020-09-22

子串

https://www.luogu.com.cn/problem/P2679

不难想到用$dp[i][j][k]$表示在 A 的前$i$个字符中取$k$个子串来匹配 B 的前$j$个字符的方案数。但是当 A 中的第$i$个字符要和前面匹配的字符连在一起时,要求第$i-1$个字符也被用来匹配,然而这个状态中既有$i-1$没有参与匹配的,也有$i-1$参与匹配的,这就没法 DP 了。

这时一般可以更细致地刻画一下状态,也就是加一维。设$dp[i][j][k][l]$表示 A 的前$i$个字符中取$k$个子串和 B 的前$j$个字符匹配,且第$i$个字符参与匹配$(l=1)$或不参与匹配$(l=0)$的方案数,那么有:

$$dp[i][j][k][0]=\begin{cases}dp[i-1][j][k][0]+dp[i-1][j][k][1]&A[i]=B[j]\\0&A[i]\not=B[j]\end{cases}$$

$$dp[i][j][k][1]=\begin{cases}dp[i-1][j-1][k][1]+dp[i-1][j-1][k-1][0]+dp[i-1][j-1][k-1][1]&A[i]=B[j]\\0&A[i]\not=B[j]\end{cases}$$

时间复杂度$O(nmk)$,足以通过,但空间开不下,滚动数组之。

#include<cstdio>

const int MOD = 1e9 + 7;
int n, m, k, dp[2][210][210][2];
char a[1010], b[210];

int main() {
    scanf("%d%d%d", &n, &m, &k);
    scanf("%s%s", a + 1, b + 1);
    dp[0][0][0][0] = dp[1][0][0][0] = 1;
    int curi = 0;
    for (int i = 1; i <= n; i++, curi ^= 1) {
        for (int j = 1; j <= m; j++) {
            for (int k = 1; k <= i && k <= j; k++) {
                dp[curi][j][k][0] = (dp[curi ^ 1][j][k][0] + dp[curi ^ 1][j][k][1]) % MOD;
                if (a[i] == b[j])
                    dp[curi][j][k][1] = ((dp[curi ^ 1][j - 1][k - 1][0] + dp[curi ^ 1][j - 1][k - 1][1]) % MOD + dp[curi ^ 1][j - 1][k][1]) % MOD;
                else dp[curi][j][k][1] = 0;
            }
        }
    }
    printf("%d\n", (dp[curi ^ 1][m][k][0] + dp[curi ^ 1][m][k][1]) % MOD);
    return 0;
}

数列

https://www.luogu.com.cn/problem/P1799

设$dp[i][j]$表示前$i$个数删了$j$个,最多有几个数在自己的位置上,则有:

$$dp[i][j]=\max(dp[i-1][j]+\Delta,dp[i-1][j-1])$$

$$\Delta=\begin{cases}1&i-j=a[i]\\0&i-j\not=a[i]\end{cases}$$

注意枚举$j$时的范围为$[0,i]$,而不是$[1,i]$。

#include<cstdio>

int n, a[1010], dp[1010][1010], ans;

inline int max(int a, int b) { return a > b ? a : b; }

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= i; j++) {
            dp[i][j] = dp[i - 1][j] + (i - j == a[i] ? 1 : 0);
            if (j) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1]);
        }
    for (int i = 1; i <= n; i++) ans = max(ans, dp[n][i]);
    printf("%d\n", ans);
    return 0;
}

释放囚犯

https://www.luogu.com.cn/problem/P1622

区间 DP。设$dp[i][j]$表示释放$[i,j]$中所有的囚犯所需最少的肉,然后对于每个状态枚举区间的分割点即可。

#include<cstdio>
#include<algorithm>

int n, m, a[110], dp[110][110];

inline int min(int a, int b) { return a < b ? a : b; }

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) scanf("%d", &a[i]);
    std::sort(a + 1, a + m + 1);
    a[m + 1] = n + 1;
    for (int l = 1; l <= m; l++)
        for (int i = 1; i + l - 1 <= m; i++) {
            int j = i + l - 1;
            dp[i][j] = 0x3fffffff;
            for (int k = i; k <= j; k++)
                dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k + 1][j] + a[j + 1] - 1 - (a[i - 1] + 1));
        }
    printf("%d\n", dp[1][m]);
    return 0;
}

2020-09-23

旅行商简化版

https://www.luogu.com.cn/problem/P1523

一个回路可以分成向东和向西两个部分,相当于两个点在同一个起点出发向东走到达同一个终点。首先按照横坐标排个序,那么下面可以 DP。设$dp[i][j]$表示这两个点一个走到了编号为$i$的点,一个走到了编号为$j$的点时当前路径的最短长度。那么有:

$$dp[i][j]=\begin{cases}dp[i-1][j]+dist(i-1,i)&j<i-1\\\min_{k=1}^{j-1}{dp[j][k]+dist(k,i)}\end{cases}$$

因为每次只有某一个点走,所以只有一个编号会发生变化,所以这样是对的。

最后要求的答案为$\min_{i=1}^{n-1}{dp[n][i]+dist(i,n)}$。

#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>

int n;
double dp[1010][1010], ans = 1e18;
struct point {
    int x, y;
    bool operator < (const point &b) const { return x < b.x; }
}points[1010];

inline double dist(int i, int j) { return sqrt((double)(points[i].x - points[j].x) * (points[i].x - points[j].x)
                                                + (double)(points[i].y - points[j].y) * (points[i].y - points[j].y)); }
inline double min(double a, double b) { return a < b ? a : b; }

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d%d", &points[i].x, &points[i].y);
    std::sort(points + 1, points + n + 1);
    memset(dp, 0x7f, sizeof dp);
    dp[2][1] = dist(1, 2);
    for (int i = 3; i <= n; i++)
        for (int j = 1; j < i; j++) {
            if (j < i - 1) dp[i][j] = dp[i - 1][j] + dist(i - 1, i);
            else for (int k = 1; k < j; k++) dp[i][j] = min(dp[i][j], dp[j][k] + dist(k, i));
        }
    for (int i = 1; i < n; i++) ans = min(ans, dp[n][i] + dist(i, n));
    printf("%.2lf\n", ans);
    return 0;
}

2020-09-24

子序列

https://www.luogu.com.cn/problem/P1410

显然,若序列中存在一个长度大于 2 的不上升序列,则不存在合法的划分方案,否则一定存在。

找一个长度大于 2 的不上升序列,可以枚举中间位置,然后对于每个中间位置找左边有没有大于等于它的数,右边有没有小于等于它的数,这样复杂度是$O(n^2)$的。

#include<cstdio>

int n, a[2010];

int main() {
    while (~scanf("%d", &n)) {
        for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
        int max = 0, secmax = 0; bool ans = true;
        for (int i = 1; i <= n && ans; i++) {
            if (max < a[i]) max = a[i];
            else if (secmax < a[i]) secmax = a[i];
            else ans = false;
        }
        if (ans) printf("Yes!\n");
        else printf("No!\n");
    }
    return 0;
}

2020-09-26

小Z的队伍排列

https://www.luogu.com.cn/problem/P2132

设$dp[i][j][k][l][m]$表示这五列分别放了$i,j,k,l,m$个数,然后直接 DP 即可。因为不确定有几排所以可以递归枚举状态。

注意开long long。

#include<cstdio>

int k, n[6], stt[6];
long long dp[31][31][31][31][31];

void dfs(int x) {
    if (x == k + 1) {
        if (stt[1]) dp[stt[1]][stt[2]][stt[3]][stt[4]][stt[5]] += dp[stt[1] - 1][stt[2]][stt[3]][stt[4]][stt[5]];
        if (stt[2]) dp[stt[1]][stt[2]][stt[3]][stt[4]][stt[5]] += dp[stt[1]][stt[2] - 1][stt[3]][stt[4]][stt[5]];
        if (stt[3]) dp[stt[1]][stt[2]][stt[3]][stt[4]][stt[5]] += dp[stt[1]][stt[2]][stt[3] - 1][stt[4]][stt[5]];
        if (stt[4]) dp[stt[1]][stt[2]][stt[3]][stt[4]][stt[5]] += dp[stt[1]][stt[2]][stt[3]][stt[4] - 1][stt[5]];
        if (stt[5]) dp[stt[1]][stt[2]][stt[3]][stt[4]][stt[5]] += dp[stt[1]][stt[2]][stt[3]][stt[4]][stt[5] - 1];
        return;
    }
    for (int i = 0; i <= n[x] && i <= stt[x - 1]; i++) stt[x] = i, dfs(x + 1), stt[x] = 0;
}

int main() {
    scanf("%d", &k);
    for (int i = 1; i <= k; i++) scanf("%d", &n[i]);
    dp[0][0][0][0][0] = 1; stt[0] = 0x3fffffff;
    dfs(1);
    printf("%lld\n", dp[n[1]][n[2]][n[3]][n[4]][n[5]]);
    return 0;
}

环状最大两段区间和

https://www.luogu.com.cn/problem/P1121

如果不考虑环状,那么可以设$f[i]$表示以第$i$个位置结尾的最大区间和,$g[i]$表示以第$i$个位置开始的最大区间和,$h[i]$表示开头在第$i$个位置以及之后的最大区间和,那么有

$$f[i]=\max(f[i-1]+a[i],a[i])$$

$$g[i]=\max(g[i+1]+a[i],a[i])$$

$$h[i]=\max(g[i],h[i+1])$$

而答案是

$$\max_{i=1}^{n-1}{f[i]+h[i+1]}$$

如果考虑环状,那么有两种情况,一种和上面一样,选择的区间没有跨过 1 和 n 之间的间隔,这时和上面一样做即可;另一种情况是选择的某个区间跨过了 1 和 n 之间的间隔,那么选的必然是开头一段,结尾一段,中间一段,这时可以发现把每个位置选与不选的状态取反后,与上面的情况等价,只是要求的变成了最小两端区间和,并且强制第 1 和第 n 个位置要选择。据此 DP 即可。

注意在第一次 DP 时,$h[n+1]$应初始化为$-\inf$。

#include<cstdio>
#include<cstring>
#include<algorithm>

int n, a[200100], f[200100], g[200100], h[200100], ans = -0x3fffffff, sum;

inline int min(int a, int b) { return a < b ? a : b; }
inline int max(int a, int b) { return a > b ? a : b; }

int main() {
    scanf("%d", &n);
    h[n + 1] = -0x3fffffff;
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), sum += a[i];
    for (int i = 1; i <= n; i++) f[i] = max(f[i - 1] + a[i], a[i]);
    for (int i = n; i >= 1; i--) g[i] = max(g[i + 1] + a[i], a[i]), h[i] = max(g[i], h[i + 1]);
    for (int i = 1; i < n; i++) ans = max(ans, f[i] + h[i + 1]);
    memset(f, 0x3f, sizeof f); memset(g, 0x3f, sizeof g); memset(h, 0x3f, sizeof h);
    for (int i = 2; i <= n; i++) f[i] = min(f[i - 1] + a[i], a[i]);
    for (int i = n - 1; i >= 2; i--) g[i] = min(g[i + 1] + a[i], a[i]), h[i] = min(g[i], h[i + 1]);
    for (int i = 2; i < n; i++) ans = max(ans, sum - f[i] - h[i + 1]);
    printf("%d\n", ans);
    return 0;
}

不等数列

https://www.luogu.com.cn/problem/P2401

设$dp[i][j]$表示长度为$i$,有$j$个小于号的排列个数,那么有

$$dp[i][j]=(j+1)dp[i-1][j]+(i-j)dp[i-1][j-1]$$

#include<cstdio>

const int MOD = 2015;
int n, k, dp[1010][1010];

int main() {
    scanf("%d%d", &n, &k);
    for (int i = 0; i <= n; i++) dp[i][0] = 1;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= k && j < i; j++) dp[i][j] = ((j + 1) * dp[i - 1][j] % MOD + (i - j) * dp[i - 1][j - 1] % MOD) % MOD;
    printf("%d\n", dp[n][k]);
    return 0;
}

https://www.luogu.com.cn/problem/P1651

可以设$dp[i][j]$表示两个塔的高度分别为$i,j$时的最大高度,那么这就是个 01 背包问题,有

$$dp[i][j]=\max(dp[i-h[k]][j],dp[i][j-h[k]],dp[i][j])$$

发现其实只关心$i,j$的相对大小关系,那么可以设$dp[i]$表示第一个塔的高度减第二个塔的高度为$i$时的第一个塔的最大高度,那么有

$$dp[i]=\max(dp[i+h[k]]+h[k],dp[i-h[k]],dp[i])$$

滚动数组即可。

#include<cstdio>
#include<cstring>

const int K = 500100;
int n, h[55], sumh, dp[2][1001000];

inline int min(int a, int b) { return a < b ? a : b; }
inline int max(int a, int b) { return a > b ? a : b; }

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &h[i]), sumh += h[i];
    memset(dp, -0x3f, sizeof dp);
    dp[1][K] = 0;
    int cur = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = -sumh; j <= sumh; j++) {
            dp[cur][j + K] = dp[cur ^ 1][j + K];
            if (j - h[i] >= -sumh) dp[cur][j + K] = max(dp[cur][j + K], dp[cur ^ 1][j - h[i] + K]);
            if (j + h[i] <= sumh) dp[cur][j + K] = max(dp[cur][j + K], dp[cur ^ 1][j + h[i] + K] + h[i]);
        }
        cur ^= 1;
    }
    if (!dp[cur ^ 1][K]) printf("-1\n");
    else printf("%d\n", dp[cur ^ 1][K]);
    return 0;
}

跑路

https://www.luogu.com.cn/problem/P1613

设$g[i][j][k]$表示$i$和$j$之间是否存在长度为$2^k$的路径,那么有

$$g[i][j][k]=||_{l=1}^ng[i][l][k-1]&&g[l][j][k-1]$$

然后所有长度为$2^k$的路径都可以看作一条长度为$1$的边,使用 Floyd 求最短路即可。

#include<cstdio>
#include<cstring>

int n, m, g[55][55][65], dis[55][55];

inline int min(int a, int b) { return a < b ? a : b; }

int main() {
    scanf("%d%d", &n, &m);
    memset(dis, 0x3f, sizeof dis);
    for (int i = 1; i <= m; i++) {
        int u, v; scanf("%d%d", &u, &v);
        g[u][v][0] = dis[u][v] = 1;
    }
    for (int l = 1; l <= 64; l++)
        for (int k = 1; k <= n; k++)
            for (int i = 1; i <= n; i++)
                for (int j = 1; j <= n; j++) g[i][j][l] = g[i][j][l] | (g[i][k][l - 1] & g[k][j][l - 1]);
    for (int l = 1; l <= 64; l++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) if (g[i][j][l]) dis[i][j] = 1;
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
    printf("%d\n", dis[1][n]);
    return 0;
}

狗哥采矿

https://www.luogu.com.cn/problem/P2380

因为只能走直路,所以可以直接枚举当前位置是一路往西走还是一路往北走,在此之前预处理前缀和即可,设$dp[i][j]$表示前$i$行前$j$列最多能采到矿的数量,那么有

$$suma[i][j]=suma[i][j-1]+a[i][j]$$

$$sumb[i][j]=sumb[i-1][j]+b[i][j]$$

$$dp[i][j]=\max(dp[i-1][j]+suma[i][j],dp[i][j-1]+sumb[i][j])$$

注意每个数据有多组输入。

#include<cstdio>
#include<cstring>

int n, m, a[510][510], b[510][510], dp[510][510], suma[510][510], sumb[510][510];

inline int max(int a, int b) { return a > b ? a : b; }

int main() {
    while (~scanf("%d%d", &n, &m)) {
        if (n == 0 && m == 0) break;
        memset(dp, 0, sizeof dp);
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]), suma[i][j] = suma[i][j - 1] + a[i][j];
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++) scanf("%d", &b[i][j]), sumb[i][j] = sumb[i - 1][j] + b[i][j];
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++) dp[i][j] = max(dp[i - 1][j] + suma[i][j], dp[i][j - 1] + sumb[i][j]);
        printf("%d\n", dp[n][m]);
    }
    return 0;
}

琪露诺

https://www.luogu.com.cn/problem/P1725

设$dp[i]$表示走到$i$时的最大权值和,直接 DP 加单调队列优化即可。

注意权值可能为负数,需要初始化 dp 数组为$-\inf$,且$dp[0]=0$。

#include<cstdio>
#include<cstring>
#include<queue>

int n, l, r, a[200100], dp[200100], ans = -0x7fffffff;
std::deque<int> q;

inline int max(int a, int b) { return a > b ? a : b; }

int main() {
    scanf("%d%d%d", &n, &l, &r);
    for (int i = 0; i <= n; i++) scanf("%d", &a[i]);
    memset(dp, -0x3f, sizeof dp);
    dp[0] = 0;
    for (int i = 0; i <= n; i++) {
        if (i - l >= 0) {
            while (!q.empty() && dp[q.back()] <= dp[i - l]) q.pop_back();
            q.push_back(i - l);
        }
        while (!q.empty() && q.front() < i - r) q.pop_front();
        if (!q.empty()) dp[i] = dp[q.front()] + a[i];
    }
    for (int i = n - r + 1; i <= n; i++) ans = max(ans, dp[i]);
    printf("%d\n", ans);
    return 0;
}
return