P2282题解
写在前面
唉,这题真的是,不愧是黑题啊!整整耗费我了两天连想带调终于做完了,也算是本蒟蒻的第一道黑题(之前的都掉紫了)。
写着题的时候,Day 1 做了这道题的弱化版,然后熬着把这道题的思路捋清了。Day 2 没起来,上课迟到,回家后开始写代码,写出来之后就是调,调了整整一下午,不知道是哪有问题,又跟题解对了对,还是没发现,当时真的是心态炸了,最后发现就是数组没初始化,真的糊了!
所以我就想写这篇题解,思路是一样的,然后增加一些我的理解。
前置知识
- DP(请先完成弱化版 P1415 拆分数列)
- 字符串哈希(或者前缀和思想)
- 线段树
题目描述(戳这里查看原题)
给定一个只有数字的字符串,要求通过添加任意多个逗号(可以为
测试数据有多组。
正文
根据弱化版 P1415 拆分数列,我们获得了一个
我们定义:
对于两个数组有如下转移方程:
(定义
这里注意。在正向 DP 完
确定优化方向
考虑优化。正向瞪眼法如果没有用,我们尝试“面向数据编程”,也就是通过我们的经验,根据题目中的数据推出我们应该拥有的复杂度。
首先,多组测试数据
因此,能给我们优化的无非两个地方,字符串比较和 DP 时枚举
优化字符串比较
在我们 naive 的做法里,字符串比较大小是
首先,在比较大小的过程中两个字符串中的前导零都是要除去的。单这一步就能把我们卡到
因此我们定义
对于位置 '0',那么就有
回归字符串。我觉得所有人最初的想法应该都是和前缀和有关。比如下面这个数字串:
"1145141919810"
我们希望能够在
因而
所以对于区间
似乎柳暗花明了?甚至连前导零都没必要考虑了,太好了吧?其实不然。注意到 unsigned long long 也只能表示大概
观察上面的式子,似乎很像哈希吧?可以自己试一试,虽然不能直接比较大小,但是可以判断两数是否相等。(此时注意将 base 值改成质数,
对于两个字符串(已经去除前导零),如果它们长度不相等,那么一定可以通过比较长度判断大小关系。如果它们长度相等,考虑 naive 做法,我们找到第一个同一位但是数字不相等的就可以比较大小了(也就是之前的数字都相等),此时是
那么我们现在就是要找到两个字符串的最长公共前缀,然后比较下一位的大小即可。我们刚才演变出的“哈希”给我们提供了
进而,我们发现去除前导零是
深入研究
还记得我们最初分析时定义的
先举个例子:
"114000000514"
容易知道
我们探究如下性质:
-
对于位置
p ,我们想让p 能包括它之前的所有前导零。那么此时p 的位置变为q 。如何通过两个数组表示q ?
对此我们分类讨论。- 若
p 前有前导零,则p-1 的位置一定是0 ,要找到最前的位置,则可以通过找到这一串0 前第一个不是0 的位置,向右+1 即可。
我们推得有q = l[p-1]+1 。 - 若
p 前没有前导零,则q = p 。但为了让它的性质普遍,我们验证上一种情况的公式是否适用。有我们设定的情况,l[p-1] = p-1 。
因而仍满足q = l[p-1]+1 = p 。
- 若
-
对于位置
p ,它所表示的不确定是否0 。我们已经求得r[p] = R_0 ,现在要逆推出可能的最靠前p 的位置q 。(即使q 能包含p 的所有前导零)
我们通过性质 1 举一反三。- 若
p 表示0 。则可以确定R_0-1 一定表示0 ,且[p,R_0-1] 是一个零串。我们想让p 包含它前面所有前导零,
根据性质 1 我们得出q = l[R_0-1]+1 。 - 若
p 不表示0 。则r[p] = R_0 = p 。同样通过性质 1,有q = l[R_0-1]+1 。
- 若
有了这些性质,我们就可以继续思考了。
优化 DP 转移
考虑之前的转移方法,填表法查之前的状态再一个一个比对是否合法更新答案。DP 的转移方法有时候会成为优化的关键。
查之前的状态因为要一次次比对,贡献可能不连续,所以不能通过诸如单调队列等方法优化。我们不妨改变一下转移方法,用前面更新后面(就是所谓的刷表法),我们尝试一下看看前面对后面有贡献的区间是否满足连续性,连续的话就可以通过一些方法优化了。
以
回想比较函数,只要
整理得:
因此,
进而,我们转移的复杂度就从
转移
不太妙。我们确定了
(我写题解的时候脑抽了一个想法,为什么更新
时间复杂度
这里主要是区分字符串比较和线段树的两个
代码
注意事项
因为有多组测试数据,所以一定要多初始化,嫌麻烦的或者想不出来无脑 memset 就完事。否则的话牢记,比较函数二分的答案要初始化为
剩下的看看代码细节就好了,主要是不太好调。
/* DP + Hash + SegTree */
#include <iostream>
#include <algorithm>
#include <cstring>
#define ls (id << 1)
#define rs (id << 1 | 1)
#define mid ((l + r) >> 1)
using namespace std;
typedef unsigned long long ull;
const int maxn = 2006;
const int base = 131;
ull Pow[maxn];
ull Hash[maxn];
char str[maxn];
int n;
int l[maxn], r[maxn];
int f[maxn], g[maxn];
struct SegmentTree{
int mx, lazy;
}tr[maxn << 2];
void init(){//初始化
Pow[0] = 1;
for (int i = 1; i < maxn; i ++){
Pow[i] = Pow[i-1] * base;
}
}
void preTreat(){//每次预处理
for (int i = 1; i <= n; i ++){
Hash[i] = Hash[i-1] * base + (str[i] - '0');
}
r[n+1] = n+1;
for (int i = 1, j = n; i <= n; i ++, j --){
l[i] = str[i] == '0' ? l[i-1] : i,
r[j] = str[j] == '0' ? r[j+1] : j;
}
}
ull getHash(int l, int r){
return Hash[r] - Hash[l-1] * Pow[r-l+1];
}
bool is_greater(int l1, int r1, int l2, int r2){//[l1,r1] > [l2,r2] ?
l1 = r[l1], l2 = r[l2]; //去除前导零
int len1 = r1 - l1 + 1, len2 = r2 - l2 + 1;
if (len1 > len2) return true;
if (len1 < len2) return false;
int L = 0, R = len1, Mid = 0, ans = 0;
while (L <= R){
Mid = (L + R) >> 1;
if (getHash(l1, l1+Mid-1) == getHash(l2, l2+Mid-1)){
ans = Mid;
L = Mid + 1;
}
else{
R = Mid - 1;
}
}
if (ans == len1) return false; //完全一样
int p1 = l1 + ans, p2 = l2 + ans;
return str[p1] > str[p2];
}
void pushup(int id){
tr[id].mx = max(tr[ls].mx, tr[rs].mx);
}
void build(int id, int l, int r, int v){
/* 一定记得清空懒标记! */
tr[id].lazy = 0;
if (l == r){
tr[id].mx = v;
return;
}
build(ls, l, mid, v);
build(rs, mid + 1, r, v);
pushup(id);
}
void pushdown(int id){
if (tr[id].lazy){
int temp = tr[id].lazy;
tr[ls].mx = max(tr[ls].mx, temp);
tr[rs].mx = max(tr[rs].mx, temp);
tr[ls].lazy = max(tr[ls].lazy, temp);
tr[rs].lazy = max(tr[rs].lazy, temp);
tr[id].lazy = 0;
}
}
void update(int id, int l, int r, int a, int b, int v){
if (a <= l && r <= b){
tr[id].mx = max(tr[id].mx, v);
tr[id].lazy = max(tr[id].lazy, v);
return;
}
pushdown(id);
if (a <= mid) update(ls, l, mid, a, b, v);
if (b > mid) update(rs, mid+1, r, a, b, v);
pushup(id);
}
int query(int id, int l, int r, int p){
if (l == r){
return tr[id].mx;
}
pushdown(id);
if (p <= mid) return query(ls, l, mid, p);
else return query(rs, mid+1, r, p);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init();
while (cin >> (str + 1)){
n = strlen(str + 1);
preTreat();
/* 正向DP */
build(1, 1, n, 1);
for (int i = 1; i <= n; i ++){
f[i] = query(1, 1, n, i);
int p = i - r[f[i]] + r[i+1];
if (!is_greater(r[i+1], p, r[f[i]], i)){
p ++;
}
if (p <= n) update(1, 1, n, p, n, i+1);
}
/* 反向DP */
build(1, 1, n, 0);
update(1, 1, n, l[f[n]-1]+1, n, n);
for (int i = f[n]; i > 0; i --){
g[i] = query(1, 1, n, i);
int p = l[max(i-1-g[i]+r[i]-1, 0)] + 1;
if (!is_greater(i, g[i], p, i-1)){
p = r[p] + 1;
}
if (p <= i-1) update(1, 1, n, p, i-1, i-1);
}
/* 输出 */
for (int i = 1; i <= n; i = g[i] + 1){
for (int j = i; j <= g[i]; j ++){
cout << str[j];
}
if (g[i] != n) cout << ',';
}
cout << endl;
}
return 0;
}
总结
反正这道题确实有水平,本蒟蒻写这道题还有它的题解也收获了很多,可能话多比较繁琐,但我觉得应该能讲得很清楚。 感谢观看!