HDU6946. Sequence 动态规划+线段树优化

时间:2021-09-01
本文章向大家介绍HDU6946. Sequence 动态规划+线段树优化,主要包括HDU6946. Sequence 动态规划+线段树优化使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

HDU6946. Sequence

题面:

问题描述:

定义:一个序列的唯一数数量为这个序列中出现且仅出现一次的数字个数。

比如,\(\{1,2,1,2\}\)唯一数数量\(0\),因为其中没有出现且仅出现一次的数字;\(\{5,6,7,6,6\}\)唯一数数量\(2\),因为\(5\)\(7\)是这个序列当中出现且仅出现一次的数字。

给定一个长度为\(N\)的序列。你需要将它们分割成\(M\)个部分(每一部分是连续的子序列),使得这\(M\)个部分的唯一数数量之和最大。

输入:

有多组测试数据。

对于每一个测试数据,第一行包括\(2\)个整数\(N\)\(M\)

第二行包括\(N\)个整数\(A_1,A_2,\dots,A_N\),表示这个序列。

\(1 \leq M,A_i \leq N\)\(2 \leq M \leq 10\)。保证所有测试数据的\(N\)之和不超过\(2\times10^5\)

输出:

对于每一个测试数据,在一行内输出一个整数,表示最大的唯一数数量之和。

样例输入:

2 2
1 1
3 2
1 1 1
4 2
1 2 2 1
6 3
1 1 2 2 3 3

样例输出:

2
1
4
4

分析:

考虑动态规划。

考虑前\(i\)个数,分成\(j\)部分的最优解。

我们发现在考虑加进第\(i\)个数的时候,它要么和第\(i-1\)个数处于同一个部分,要么自己成为新的一部分的第\(1\)个数。

对于第二种情况,新的一部分的贡献,直接为\(1\)即可。

对于第一种情况,第\(i\)个数对这一部分产生的贡献需要根据这一部分的数字情况进行考虑。不妨设这一部分的第\(1\)个数在整体的位置为\(k\)

  1. \(k\)\(i-1\)位置上没有出现第\(i\)个数,则第\(i\)个数的贡献为\(1\)
  2. \(k\)\(i-1\)位置上出现一次第\(i\)个数,则第\(i\)个数的贡献为\(-1\)
  3. \(k\)\(i-1\)位置上出现两次及以上次第\(i\)个数,则第\(i\)个数的贡献为\(0\)

我们不妨这样定义状态,设\(g(i,j,k)\)为考虑前\(i\)个数,分成\(j\)个部分,最后一部分的起始位置为\(k\)的最优解\((j \leq k\leq i)\)

设此时\(i\)位置的贡献为\(v(k,i)(j \leq k<i)\),则有如下转移方程:

\[\begin{aligned} g(i,j,k)&=g(i-1,j,k)+v(k,i),& j \leq k<i\\ g(i,j,k)&=\max\limits_{j-1\leq x \leq i-1}\{g(i-1,j-1,x)\}+1,& k=i \end{aligned} \]

先不谈\(v\)计算的问题,光转移就需要\(O(n^2 m)\)的复杂度,这显然是无法接受的。考虑优化。

我们发现对于第\(i\)个位置的数,随着\(k\)由小变大,贡献是一段为\(0\)(可能没有),一段\(-1\)(可能没有),一段\(1\)(可能没有)这样分布的,分段点也很明显。不妨设从第\(i\)个位置的数往前,第\(1\)次再出现这个数的位置为\(p_1(i)\),第\(2\)次再出现这个数的位置为\(p_2(i)\),则有如下式子:

\[v(k,i)=\left\{\begin{aligned} 0&,&j \leq k& \leq p_2(i)\\ -1&,&p_2(i)<k&\leq p_1(i)\\ 1&,&p_1(i)<k&<i \end{aligned} \right. \]

而且在上面的转移方程中\(i\)只和\(i-1\)相关,可以利用滚动数组的思想去掉\(g\)中的\(i\)

同时我们可以定义\(f(i,j)=\max\limits_{j \leq x \leq i}\{g(i,j,x)\}\),显然\(f(i,j)\)的意义就是考虑前\(i\)个字符,分成\(j\)个部分的最优解。

上面的转移方程可以变成,

\[\text{对于给定$j$,$i$从$j$向$n$遍历}\\ \begin{aligned} g(j,k)&=g(j,k)-1,&p_2(i)<k&\leq p_1(i)\\ g(j,k)&=g(j,k)+1,&p_1(i)<k&<i\\ g(j,k)&=f(i-1,j-1)+1,& k&=i\\ f(i,j)&=\max\limits_{j \leq m \leq i}\{g(j,m)\}\\ \end{aligned} \]

这已经很显然了,我们需要一个支持区间修改,单点修改,维护区间最大值的数据结构,线段树即可胜任。

\(p_1(i)\)\(p_2(i)\)均可以\(O(n)\)预处理,遍历\(j\)\(O(m)\)的,遍历\(i\)\(O(n)\)的,转移的前\(2\)个式子涉及区间修改,是\(O(\log n)\)的,第\(3\)个式子涉及单点修改,是\(O(\log n)\)的,第\(4\)个式子涉及区间查询,是\(O(\log n)\)的,所以单次转移是\(O(\log n)\)的。最终复杂度即为\(O(nm \log n)\)

代码:

⚠ 由于某些原因,这里的f[maxm][maxn]第一维j第二维i,即\(f(i,j)\)表示为f[j][i]

#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
#define ls nodes[rt << 1]
#define rs nodes[rt << 1 | 1]
#define fa nodes[rt]
const int maxn = 2e5 + 10;
const int maxm = 15;
int pre1[maxn], pre2[maxn];
int now[maxn];
int a[maxn];
int n, m;
struct Node {
    int l, r;
    int max_num;
    int tag;
};
struct SegTree {
    Node nodes[maxn << 2];
    void build(int rt, int l, int r) {
        fa.l = l, fa.r = r;
        fa.max_num = fa.tag = 0;
        if (l == r)
            return;
        int mid = l + r >> 1;
        build(rt << 1, l, mid);
        build(rt << 1 | 1, mid + 1, r);
    }
    void update_node(int rt, int val) {
        fa.max_num += val;
        fa.tag += val;
    }
    void pushup(int rt) { fa.max_num = max(ls.max_num, rs.max_num); }
    void pushdown(int rt) {
        if (fa.tag) {
            update_node(rt << 1, fa.tag);
            update_node(rt << 1 | 1, fa.tag);
            fa.tag = 0;
        }
    }
    void update(int rt, int L, int R, int val) {
        if (L <= fa.l && fa.r <= R) {
            update_node(rt, val);
            return;
        }
        pushdown(rt);
        if (L <= ls.r)
            update(rt << 1, L, R, val);
        if (R >= rs.l)
            update(rt << 1 | 1, L, R, val);
        pushup(rt);
    }
    int query(int rt, int L, int R) {
        if (L <= fa.l && fa.r <= R) {
            return fa.max_num;
        }
        int res = 0;
        pushdown(rt);
        if (L <= ls.r)
            res = max(res, query(rt << 1, L, R));
        if (R >= rs.l)
            res = max(res, query(rt << 1 | 1, L, R));
        return res;
    }
} seg;
int f[maxm][maxn];
int main() {
    while (scanf("%d%d", &n, &m) == 2) {
        for (int i = 1; i <= n; i++) {
            scanf("%d", a + i);
        }
        memset(now, 0, sizeof(int) * (n + 10));
        memset(pre1, 0, sizeof(int) * (n + 10));
        memset(pre2, 0, sizeof(int) * (n + 10));
        for (int i = 1; i <= n; i++) {
            pre1[i] = now[a[i]];
            pre2[i] = pre1[pre1[i]];
            now[a[i]] = i;
        }
        int res = 0;
        // 注意f[1][i]需要单独计算
        for (int i = 1; i <= n; i++) {
            if (pre2[i]) {
                ;
            } else if (pre1[i]) {
                res--;
            } else {
                res++;
            }
            f[1][i] = res;
        }
        for (int j = 2; j <= m; j++) {
            seg.build(1, 1, n);
            for (int i = j; i <= n; i++) {
                // pre1[i]+1, i-1    : +1
                // pre2[i]+1, pre1[i]: -1
                if (pre1[i] + 1 <= i - 1) {
                    if (pre1[i] + 1 >= j) {
                        seg.update(1, pre1[i] + 1, i - 1, 1);
                    } else if (i - 1 >= j) {
                        seg.update(1, j, i - 1, 1);
                    }
                }
                if (pre2[i] + 1 <= pre1[i]) {
                    if (pre2[i] + 1 >= j) {
                        seg.update(1, pre2[i] + 1, pre1[i], -1);
                    } else if (pre1[i] >= j) {
                        seg.update(1, j, pre1[i], -1);
                    }
                }
                seg.update(1, i, i, f[j - 1][i - 1] + 1);
                f[j][i] = seg.query(1, j, i);
            }
        }
        printf("%d\n", f[m][n]);
    }
    return 0;
}

原文地址:https://www.cnblogs.com/Bamboo-Wind/p/15214165.html