The 2019 Asia Nanchang Online Programming Contest C. Hello 2019(区间DP + 线段树分治 + 矩阵)

The 2019 Asia Nanchang Online Programming Contest C. Hello 2019

题目

给一个 2e5 的字符串,有 2e5 的询问。每次询问一个区间【l,r】,问区间里至少删除多少个字符才能保证不含子串 “8102”,但是要含 “9201”。如果不能满足要求输出 -1。

分析

其实这个题是之前 cf 原题的改编,CF #750E
cf 原题大意是不含 “2016”,要含“2017”。先考虑简单 cf 版本的:

如果是单次询问的话可以用区间 DP来做,因为要求删去字符数最小,可以将删去一个字符代价看作 1。

定义状态 0 1 2 3 4 表示 “”,“2”,“20”,“201”,“2017”。对于每个位置的状态转移用矩阵表示: D = [ a 0 , 0 . . . a 0 , 4 a i , j − 1 a i , j a i , j + 1 a 4 , 0 . . . a 4 , 4 ] D= \left[ \begin{matrix} a_{0,0} & ... & a_{0,4} \\ a_{i,j-1} & a_{i,j} & a_{i,j+1} \\ a_{4,0} & ... & a_{4,4} \end{matrix} \right] D=a0,0ai,j1a4,0...ai,j...a0,4ai,j+1a4,4
其中 a i , j a_{i,j} ai,j 表示从 i i i 状态转移到 j j j 状态需要的代价(经过当前位置)。对于每个字符,初始化的时候全部矩阵值为无穷,然后将对角线位置初始化为 0。

举个例子,假设当前位置是 2,那么矩阵为: D = [ 1 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 0 ] D= \left[ \begin{matrix} 1 & 0 & \inf & \inf & \inf \\ \inf & 0 & \inf & \inf & \inf \\ \inf & \inf & 0 & \inf & \inf \\ \inf & \inf & \inf & 0 & \inf \\ \inf & \inf & \inf & \inf & 0 \end{matrix} \right] D=1infinfinfinf00infinfinfinfinf0infinfinfinfinf0infinfinfinfinf0

因为从 “” 到 “2” 不需要代价,反而从 “” 保持 “” 需要删去当前字符 2 。当前字符是 0、1、7 同理。

当前字符是 6 时,为了满足题目要求。那么状态 3、4 (带有 201)保持原来状态需要花费1 代价。即: D = [ 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 0 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 1 inf ⁡ inf ⁡ inf ⁡ inf ⁡ inf ⁡ 1 ] D= \left[ \begin{matrix} 0 & \inf & \inf & \inf & \inf \\ \inf & 0 & \inf & \inf & \inf \\ \inf & \inf & 0 & \inf & \inf \\ \inf & \inf & \inf & 1 & \inf \\ \inf & \inf & \inf & \inf & 1 \end{matrix} \right] D=0infinfinfinfinf0infinfinfinfinf0infinfinfinfinf1infinfinfinfinf1

上面是区间为 1 矩阵的情况,对于两个区间合并这里重载矩阵乘法,我们知道矩阵乘法是: c i , j = ∑ k = 1 n a i , k ∗ b k , j c_{i,j}=\sum_{k=1}^{n}a_{i,k}*b_{k,j} ci,j=k=1nai,kbk,j这里因为要求最小代价,所以改成: c i , j = m i n { a i , k + b k , j } , k ∈ [ 1 , n ] c_{i,j}=min\{a_{i,k}+b_{k,j}\},k\in[1,n] ci,j=min{ai,k+bk,j},k[1,n]上面其实就是区间 DP 的状态转移了,可以类比 Floyd 的思想。

这样就解决了单次询问,对于多次询问,可以用线段树区间查询分治来做,也就是维护每个一区间的矩阵状态。(因为区间DP支持区间合并)

这次南昌的题可以倒过来看,相应的区间也要倒过来。就转化为了 cf 的原题。

扩展:增加单点修改,和每个位置对应的代价,求最小代价。

参考:
https://www.cnblogs.com/BCOI/p/10329108.html
https://www.cnblogs.com/heyuhhh/p/11491321.html
https://www.cnblogs.com/yyf0309/p/8475433.html

cf 题目
#include 
#define INF 0x3f3f3f3f
#define fuck(x) cout << (x) << endl
#define lson l, m, rt<<1
#define rson m+1, r, rt<<1|1
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int N = 2e5 + 5;

int n, q;
char s[N];

struct mat{
    int a[5][5];
    mat() { memset(a, INF, sizeof(a)); }
    mat operator * (const mat& b) const{
        mat c;
        for(int i = 0; i < 5; i++)
            for (int j = 0; j < 5; j++)
                for(int k = 0; k < 5; k++)
                    c.a[i][j] = min(c.a[i][j], a[i][k] + b.a[k][j]);
        return c;
    }
} tr[N<<2];     // 线段树维护区间矩阵状态。

void build(int l, int r, int rt){   // 线段树初始化也就是 DP 矩阵初始化
    if(l == r){
        for(int i = 0; i < 5; i++)
            tr[rt].a[i][i] = 0;
        if(s[l] == '2') tr[rt].a[0][0] = 1, tr[rt].a[0][1] = 0;
        if(s[l] == '0') tr[rt].a[1][1] = 1, tr[rt].a[1][2] = 0;
        if(s[l] == '1') tr[rt].a[2][2] = 1, tr[rt].a[2][3] = 0;
        if(s[l] == '7') tr[rt].a[3][3] = 1, tr[rt].a[3][4] = 0;
        if(s[l] == '6') tr[rt].a[3][3] = 1, tr[rt].a[4][4] = 1;
        return;
    }
    int m = l + r >> 1;
    build(lson);
    build(rson);
    tr[rt] = tr[rt << 1] * tr[rt << 1 | 1];     // pushup
}

mat query(int l, int r, int rt, int L, int R){  // 区间查询
    if(L <= l && r <= R) return tr[rt];
    int m = l + r >> 1;
    if(R <= m)  // 全在左边
        return query(lson, L, R);   
    if(m < L)   // 全在右边
        return query(rson, L, R);
    return query(lson, L, R) * query(rson, L, R);
}

int main(){
    scanf("%d%d %s", &n, &q, s+1);
    build(1, n, 1);
    for (int i = 1, x, y; i <= q; i++){
        scanf("%d%d", &x, &y);
        mat ans = query(1, n, 1, x, y);
        int res = ans.a[0][4] > n ? -1 : ans.a[0][4];
        printf("%d\n", res);
    }
    return 0;
}

南昌题目:
#include 
#define INF 0x3f3f3f3f
#define fuck(x) cout << (x) << endl
#define lson l, m, rt<<1
#define rson m+1, r, rt<<1|1
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int N = 2e5 + 5;

int n, q;
char s[N], t[N];

struct mat{
    int a[5][5];
    mat() { memset(a, INF, sizeof(a)); }
    mat operator * (const mat& b) const{
        mat c;
        for(int i = 0; i < 5; i++)
            for (int j = 0; j < 5; j++)
                for(int k = 0; k < 5; k++)
                    c.a[i][j] = min(c.a[i][j], a[i][k] + b.a[k][j]);
        return c;
    }
} tr[N<<2];

void build(int l, int r, int rt){
    if(l == r){
        for(int i = 0; i < 5; i++)
            tr[rt].a[i][i] = 0;
        if(s[l] == '2') tr[rt].a[0][0] = 1, tr[rt].a[0][1] = 0;
        if(s[l] == '0') tr[rt].a[1][1] = 1, tr[rt].a[1][2] = 0;
        if(s[l] == '1') tr[rt].a[2][2] = 1, tr[rt].a[2][3] = 0;
        if(s[l] == '9') tr[rt].a[3][3] = 1, tr[rt].a[3][4] = 0;
        if(s[l] == '8') tr[rt].a[3][3] = 1, tr[rt].a[4][4] = 1;
        return;
    }
    int m = l + r >> 1;
    build(lson);
    build(rson);
    tr[rt] = tr[rt << 1] * tr[rt << 1 | 1];     // pushup
}

mat query(int l, int r, int rt, int L, int R){
    if(L <= l && r <= R)
        return tr[rt];
    int m = l + r >> 1;
    if(R <= m)  // 全在左边
        return query(lson, L, R);   
    if(m < L)   // 全在右边
        return query(rson, L, R);
    return query(lson, L, R) * query(rson, L, R);
}

int main(){
    scanf("%d%d %s", &n, &q, t+1);
    for(int i = 1; i <= n; i++){
        s[i] = t[n - i + 1];
    }
    build(1, n, 1);
    for (int i = 1, x, y; i <= q; i++){
        scanf("%d%d", &x, &y);
        mat ans = query(1, n, 1, n-y+1, n-x+1);
        int res = ans.a[0][4] > n ? -1 : ans.a[0][4];
        printf("%d\n", res);
    }
    return 0;
}


你可能感兴趣的:(比赛,线段树,区间dp)