[线性DP] UVa1625 颜色长度 (LRJ代码)(多状态DP)

题目

这里写图片描述

思路

1.本题可以说是有难度了,首先思路就不好想到,DP有两个状态,两个状态分别有自己的状态转移方程。其次代码实现起来,细节很多,自己尝试写了一下,debug了一下午也没搞出来,只能搬来LRJ的代码了。


2.回顾一下本题的思考过程:
首先,可以想到LCS,因为每次都是从两个序列种的一个拿元素。最基本的状态就想到了,d(i,j),表示从两个序列分别拿走i,j个元素后最小的指标函数值。问题就出在指标函数的上,本题的指标函数应当是L(c)之和,比较复杂。
容易想到的方式是,让状态变成多维,记录每个元素开始的地方,从而拿进元素时算出L(c)之和。但这样的话时间和空间都无法承受。
正确的方法是,再来一个状态c(i,j),表示已经开始但没有结束的颜色数量,从而可以根据上一个d(i2,j2)来一步算出这次的d(i1,j1),这样的话就引入了状态c(i,j)作为辅助计算的状态,同时这个状态c(i,j)也需要转移计算。


3.状态定义:

  • d(i,j):从两个序列拿走i,j组成的子序列,最小的L(c)之和。
  • c(i,j):从两个序列拿走i,j组成的子序列,有多少颜色已经开始但没有结束。

4.初状态:都是0
5.答案:d(n,m)
6.状态转移方程:

d(i,j)=min{d(i1,j)+c(i1,j),d(i,j1)+c(i,j1)} d ( i , j ) = m i n { d ( i − 1 , j ) + c ( i − 1 , j ) , d ( i , j − 1 ) + c ( i , j − 1 ) }

c(i,j)={c(i1,j)+1|element(i,j) is a startcolor}or{c(i1,j)1|element(i,j) is an endcolor}or {c(i,j1)+1|element(i,j) is a startcolor}or {c(i1,j1)1|element(i,j) is an endcolor} c ( i , j ) = { c ( i − 1 , j ) + 1 | e l e m e n t ( i , j )   i s   a   s t a r t c o l o r } o r { c ( i − 1 , j ) − 1 | e l e m e n t ( i , j )   i s   a n   e n d c o l o r } o r   { c ( i , j − 1 ) + 1 | e l e m e n t ( i , j )   i s   a   s t a r t c o l o r } o r   { c ( i 1 , j − 1 ) − 1 | e l e m e n t ( i , j )   i s   a n   e n d c o l o r }



7.滚动数组:
本题的滚动数组设计的时候,还是要机灵一下的。
滚动数组是两维的,为什么不能设置成一维呢?
因为滚动数组的原理是,利用某个旧元素创造一个新元素,再用这个新元素将某个旧元素覆盖。
在本题中,i-1的转移是常见的转移,只有当j逆序枚举时才能正常工作。
而j-1的转移也在同时发生,只有当j正序枚举时才能正常工作。
所以这两个转移,用滚动数组时冲突了,所以我们设置成两维,一维保存i-1的数据,一维保存新数据,并在新数据的一维正序枚举。

根据本人的验证,并不需要将滚动数组二维化就能AC。原因是j-1要求正序枚举,而i-1实际对枚举顺序无要求,所以正序枚举即可。


并且LRJ使用了“^”,简化了很多代码,这里要学习这个“^”的使用:

^,按位异或运算符。
参与运算的两个值,如果两个相应位相同,则结果为0,否则为1。即:0^0=0, 1^0=1, 0^1=1, 1^1=0
x^1的作用:当x为0时返回1,当x为1时返回0。有点类似于!,但只会返回0和1。

代码

LRJ原版

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define _for(i,a,b) for(int i = (a); i<(b); i++)
#define _rep(i,a,b) for(int i = (a); i<=(b); i++)
using namespace std;

const int INF = 100000000;
const int maxn = 5000 + 5;

char p[maxn], q[maxn];  // 从位置1开始
int sp[26], ep[26], sq[26], eq[26];  // sq:start position of q
int d[2][maxn], c[2][maxn];   // 使用滚动数组,注意这里的滚动数有两层

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%s%s", p + 1, q + 1);

        int n = strlen(p + 1);
        int m = strlen(q + 1);
        _rep(i, 1, n) p[i] -= 'A';
        _rep(i, 1, m) q[i] -= 'A';

        // 计算s和e
        _for(i, 0, 26) { sp[i] = sq[i] = INF; ep[i] = eq[i] = 0; }  // 此处的初始化很关键
        _rep(i, 1, n) {
            sp[p[i]] = min(sp[p[i]], i);
            ep[p[i]] = i;
        }
        _rep(i, 1, m) {
            sq[q[i]] = min(sq[q[i]], i);
            eq[q[i]] = i;
        }

        // dp
        int t = 0;
        memset(c, 0, sizeof(c));
        memset(d, 0, sizeof(d));
        _rep(i, 0, n) {
            _rep(j, 0, m) {
                if (i == 0 && j == 0) continue;

                // 计算d
                int v1 = INF, v2 = INF;
                if (i) v1 = d[t ^ 1][j] + c[t ^ 1][j];
                if (j) v2 = d[t][j - 1] + c[t][j - 1];
                d[t][j] = min(v1, v2);

                // 计算c
                if (i) {
                    c[t][j] = c[t ^ 1][j];
                    if (sp[p[i]] == i && sq[p[i]] > j) c[t][j]++;
                    if (ep[p[i]] == i && eq[p[i]] <= j) c[t][j]--;
                }
                else if (j) {
                    c[t][j] = c[t][j - 1];
                    if (sq[q[j]] == j && sp[q[j]] > i) c[t][j]++;
                    if (eq[q[j]] == j && ep[q[j]] <= i) c[t][j]--;
                }
            }
            t ^= 1;
        }
        printf("%d\n", d[t ^ 1][m]);
    }

    return 0;
}

滚动数组去二维

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define _for(i,a,b) for(int i = (a); i<(b); i++)
#define _rep(i,a,b) for(int i = (a); i<=(b); i++)
using namespace std;

const int INF = 100000000;
const int maxn = 5000 + 5;

char p[maxn], q[maxn];  // 从位置1开始
int sp[26], ep[26], sq[26], eq[26];  // sq:start position of q
int d[maxn], c[maxn];   // 使用滚动数组,注意这里的滚动数有两层

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%s%s", p + 1, q + 1);

        int n = strlen(p + 1);
        int m = strlen(q + 1);
        _rep(i, 1, n) p[i] -= 'A';
        _rep(i, 1, m) q[i] -= 'A';

        // 计算s和e
        _for(i, 0, 26) { sp[i] = sq[i] = INF; ep[i] = eq[i] = 0; }  // 此处的初始化很关键
        _rep(i, 1, n) {
            sp[p[i]] = min(sp[p[i]], i);
            ep[p[i]] = i;
        }
        _rep(i, 1, m) {
            sq[q[i]] = min(sq[q[i]], i);
            eq[q[i]] = i;
        }

        // dp
        memset(c, 0, sizeof(c));
        memset(d, 0, sizeof(d));
        _rep(i, 0, n) {
            _rep(j, 0, m) {
                if (i == 0 && j == 0) continue;

                // 计算d
                int v1 = INF, v2 = INF;
                if (i) v1 = d[j] + c[j];
                if (j) v2 = d[j - 1] + c[j - 1];
                d[j] = min(v1, v2);

                // 计算c
                if (i) {
                    c[j] = c[j];
                    if (sp[p[i]] == i && sq[p[i]] > j) c[j]++;
                    if (ep[p[i]] == i && eq[p[i]] <= j) c[j]--;
                }
                else if (j) {
                    c[j] = c[j - 1];
                    if (sq[q[j]] == j && sp[q[j]] > i) c[j]++;
                    if (eq[q[j]] == j && ep[q[j]] <= i) c[j]--;
                }
            }
        }
        printf("%d\n", d[m]);
    }

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单调队列优化DP是一种常用的优化方法,可以将时间复杂度从 $O(n^2)$ 降低到 $O(n)$ 或者 $O(n \log n)$。以下是一道利用单调队列优化DP的典型题目: 题目描述: 给定一个长度为 $n$ 的序列 $a_i$,定义 $f(i)$ 为 $a_i$ 到 $a_n$ 中的最小值,即 $f(i) = \min\limits_{j=i}^n a_j$。现在定义 $g(i)$ 为满足 $f(j) \ge a_i$ 的最小下标 $j$,即 $g(i) = \min\{j \mid j > i, f(j) \ge a_i\}$。如果不存在这样的下标 $j$,则 $g(i) = n+1$。 现在请你计算出 $1 \le i \le n$ 的所有 $g(i)$ 的值。 输入格式: 第一行包含一个整数 $n$。 第二行包含 $n$ 个整数 $a_1,a_2,\cdots,a_n$。 输出格式: 输出 $n$ 行,第 $i$ 行输出 $g(i)$ 的值。 输入样例: 5 3 1 2 4 5 输出样例: 2 5 5 5 6 解题思路: 设 $dp(i)$ 示 $g(i)$,那么 $dp(i)$ 与 $dp(i+1)$ 的转移关系可以示为: $$dp(i)=\begin{cases}i+1, &\text{if}\ f(i+1)\ge a_i \\dp(i+1), &\text{else}\end{cases}$$ 这个转移方程可以使用暴力 DP 解决,时间复杂度为 $O(n^2)$。但是,我们可以使用单调队列优化 DP,将时间复杂度降为 $O(n)$。 我们定义一个单调队列 $q$,存储下标。队列 $q$ 中的元素满足: - 队列中的元素是单调递减的,即 $q_1 < q_2 < \cdots < q_k$; - 对于任意的 $i\in [1,k]$,有 $f(q_i) \ge f(q_{i+1})$。 队列 $q$ 的作用是维护一个长度为 $k$ 的区间 $[i+1,q_k]$,满足这个区间中的所有 $j$ 都满足 $f(j) < f(i+1)$。 根据定义,当我们要求 $dp(i)$ 时,只需要查找队列 $q$ 中第一个满足 $f(q_j) \ge a_i$ 的位置 $q_j$,那么 $g(i) = q_j$,如果队列 $q$ 中不存在这样的位置,则 $g(i) = n+1$。 那么如何维护单调队列 $q$ 呢?我们可以在每次 DP 的过程中,将 $i$ 加入队尾。然后判断队首元素 $q_1$ 是否满足 $f(q_1) \ge a_i$,如果满足则弹出队首元素,直到队首元素不满足条件为止。 由于每个元素最多被加入队列一次,并且最多被弹出一次,因此时间复杂度为 $O(n)$。具体实现细节可以参考下面的代码实现:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值