前言

题目链接:洛谷

题意简述

长度为 \(n\) 的一串项链,每颗珠子是 \(k\) 种颜色之一。第 \(i\) 颗与第 \(i-1, i+1\) 颗珠子相邻,第 \(n\) 颗与第 \(1\) 颗也相邻。切两刀,把项链断成两条链。要求每种颜色的珠子只能出现在其中一条链中。求方案数量(保证至少存在一种),以及切成的两段长度之差绝对值的最小值。

题目分析

据我所知,这道题有如下做法:

  1. 线段树 + 增量法 + 最值 / 二分
  2. 哈希 + 双指针
  3. 分治 + 双指针

写一篇题解以总结并加深印象。

方法 \(1\):线段树 + 增量法 + 最值 / 二分

分别对于每种颜色考虑。对于一种颜色,把这个环分成了若干段,我们要切一定是截取某一段的一部分,要不然就会跨过一个位置,不合法。暴力的想法是枚举一个端点,对每种颜色分别考虑,把合法的位置标记。最后被标记了 \(k\) 次的位置就是合法的位置。

优化也很简单,对于两个相邻的位置,需要更改的地方并不多。考虑增量法,把端点 \(i - 2 \sim i - 1\) 移动到 \(i - 1 \sim i\),只会对 \(i - 1\) 这个位置对应的颜色 \(col_{i - 1}\) 造成影响。我们把上一次出现 \(col_{i - 1}\) 的位置到 \(i - 2 \sim i - 1\) 减 \(1\),再把 \(i - 1 \sim i\) 到下一次出现的位置之间加 \(1\),就完成了更新。

这很线段树啊,先把边权右放,即用 \(i\) 这个位置的值代表 \(i - 1 \sim i\) 这条边,修改变成了区间加。第一个问题,即方案数是求区间最值和区间最值数量,很简单。

对于第二个问题,为了让答案尽量小,即让其中一段尽可能接近 \(\cfrac{n}{2}\),我们先找到那个位置 \(p\),这时候序列被 \(p\) 和 \(i\) 分成了三个部分,对于每个部分求区间最值出现的最靠近 \(p\) 的位置,容易发现就是最左边或最右边的位置,计算下距离,对答案取个 \(\min\) 即可。注意,那个部分的最值必须也要满足要求才能对答案有贡献。当然,也可以线段树上二分出最左边或最右边的位置。

此外,注意有些颜色可能在序列中只出现了一次,我们无论怎么切都不会使它们不合法,所以实现的时候特判一下就好了。以及环上的处理略为繁琐。

时间复杂度:\(\Theta(n \log n)\),瓶颈在线段树,但逝大常数,要卡卡常。

方法 \(2\):哈希 + 双指针

如果进一步分析问题很容易发现,环是一个幌子,我们只需要找到一段区间,对于每种颜色,要么全都在里面出现了,要么一次都没有出现,那么截取出来一定是合法的。因为对于令一半,每种颜色也可能是全都出现了或者一次都没有出现。

我们可以设计出一个哈希函数,对于每一种颜色全部出现和一次都没出现出现是等价的。

异或哈希:如果某种颜色出现了 \(c\) 次,让前 \(c - 1\) 出现的位置的权值设置成随机数,最后一位设置成前 \(c - 1\) 位的权值异或和。这样,如果有一个区间完全包含或完全不包含所有的位置,对于这一种颜色,异或和均为 \(0\)。当然,对于一个区间里的每一个颜色异或和都为 \(0\) 的话,这就是一个合法区间。哈希嘛,不需要考虑不同颜色之间的影响。于是问题变成了统计有多少区间异或和为 \(0\)。搞个前缀异或和,变成了前缀异或和相等的所有位置之间任意取构成的区间。如果某一种全缀和出现了 \(x\) 次,那么方案数增加 \(\cfrac{x(x - 1)}{2}\)。第二个问题,就是最接近 \(\cfrac{n}{2}\) 的两个位置,排序后双指针即可。

和哈希:设计一个哈希函数 \(F(x)\),对于一个局面我们定义其权值为 \(\sum \limits _ {i = 1} ^ {k} yzh[i] \cdot F(cnt[i])\),其中 \(yzh[i]\) 为每一种颜色对应的随机权,\(cnt[i]\) 为颜色 \(i\) 在这个局面出现的次数。\(F(x)\) 应满足对于 \(x = 0\) 和 $x = $ 颜色 \(i\) 在整个序列出现的总次数,返回值相同,对于其他 \(x\) 返回个随机值即可。那么不就和异或哈希后续处理一样了吗?

时间复杂度:\(\Theta(n \log n)\),瓶颈在排序,但挺快的。

方法 \(3\):分治 + 双指针

首先来考虑简化模型。对于相互包含的颜色,如 \(\texttt{A} \cdots \texttt{B} \cdots \texttt{A} \cdots \texttt{B}\),我们既要满足放在一段 \(\texttt{A}\) 中,也要满足放在一段 \(\texttt{B}\) 中,那么综合考虑,\(\texttt{A}\) 和 \(\texttt{B}\) 是没有区别的,那为什么不合并起来呢?发现简化后的模型,一种颜色只会出现在另一种颜色的某一段中。

不妨考虑一个颜色 \(\texttt{A}\),它出现在序列上是形如 \(\texttt{A} \cdots \texttt{A} \cdots \texttt{A}\) 的,不妨记作 \(f(\texttt{A})\),那么我们选取的一段一定是在相邻的一段 \(\texttt{A} \cdots \texttt{A}\) 中。我们分别考虑每一段。

每一段是形如 \(\texttt{A} f(\texttt{B}) f(\texttt{C}) \cdots f(\texttt{K}) \texttt{A}\) 的。显然被分成了若干子问题,递归解决每一个 \(f(\texttt{B} \cdots \texttt{K})\),接下来只用考虑选出的区间是完整的若干连续的子段,如 \(f(\texttt{B})\),\(f(\texttt{B}) f(\texttt{C})\) 等等。设 \(f(\texttt{B}) f(\texttt{C}) \cdots f(\texttt{K})\) 有 \(k\) 个子问题。那么对于当前对方案数的贡献是在 \(k + 1\) 个点中选出两个,其中构成的连续段就是选出段,方案数是 \(\cfrac{(k + 1) k}{2}\)。第二个问题同之前解决方法一样,双指针即可。

分治部分很好处理,那来说说预处理合并颜色怎么搞。不妨设 \(nxt_i\) 表示下一次出现 \(i\) 这个位置的颜色的位置,如果没有则是 \(0\)。那么对于相交的两个颜色,这样一段:\(j \cdots i \cdots nxt_j \cdots nxt_i\),即对于 \(i\),\(\exists j \in [1, i - 1] \land nxt_j \in (i, nxt_i)\)。对于前一个条件,我们从左到右扫过来,对于当前的 \(i\),只用考虑之前被扫过的 \(j\)。对于后一个条件,先考虑一个 \(nxt\) 从顶到底递增的单调栈,每次先把 \(nxt_j \leq i\) 的弹栈弹掉,对于剩下的元素,和 \(< nxt_i\) 的每个合并一下,但是时间复杂度会假。如果要保证时间复杂度,即每个元素只被加进去一次,弹出去一次,我们在访问的时候就要弹掉。

对于 \(i\) 之后的一个 \(k\),如果 \(i\) 和 \(k\) 满足交叉,那么这么做没有什么问题。但是如果有 \(nxt_k \leq nxt_i\) 并且存在一个被弹掉的 \(j\),满足 \(nxt_k > nxt_j\),就会出现问题,可见下图。

当然,类似红绿关系的可以不止两种,可以继续套。我们用上述单调栈合并后的结果是下图。

发现,合并后,如果还存在上述遗留情况的交叉,会越过处理后同一种颜色的相邻两段。而在处理前遗留情况是越过某一颜色的最后一次出现的位置。所以再用相同的算法处理一遍即可。

至于颜色的合并,使用并查集即可。

至此,我们完成了本题的求解。时间复杂度 \(\Theta(n \alpha (k))\)。瓶颈在于并查集。

代码

均给出了详细注释,供大家参考。

方法 \(1\):线段树 + 增量法 + 最值 / 二分

只写了最值版本的,代码,跑得很慢

方法 \(2\):哈希 + 双指针

异或哈希

代码\(528\) 毫秒

和哈希

代码\(1.43\) 秒

方法 \(3\):分治 + 双指针

最优解,才 \(200\) 毫秒左右。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std; const int MAX = 1 << 25;
char buf[MAX], *p = buf; #define isdigit(x) ('0' <= x && x <= '9')
#define getchar() *p++
#define check(x) ans = min(ans, abs(n - ((x) << 1))) inline void read(int &x) {
char ch = 0; x = 0;
for (; !isdigit(ch); ch = getchar());
for (; isdigit(ch); x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar());
} const int N = 1e6 + 10;
const int inf = 0x3f3f3f3f; int n, k, val[N], ans = inf;
long long cnt; int fa[N], rk[N];
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
void merge(int a, int b) {
if ((a = get(a)) == (b = get(b))) return;
if (rk[a] < rk[b]) swap(a, b);
fa[b] = a;
if (rk[a] == rk[b]) ++rk[a];
}
// 并查集 int fir[N], lst[N], nxt[N], pre[N]; inline int trans(int x) { return x ^ n ? x + 1 : 1; } // x 的下一个位置
inline int snart(int x) { return x ^ 1 ? x - 1 : n; } // x 的上一个位置
inline int dis(int x, int y) { return x - y + 1; } // y ~ x 这样一段的距离 void solve(int x) {
for (int i = trans(x); i != nxt[x]; i = trans(pre[i])) // i 是这种颜色出现的第一个位置,pre[i] 就是最后一个位置
for (int j = i; nxt[j] != i; j = nxt[j]) // 把这种颜色每一段分治解决
solve(j);
int cnt = 0;
for (int i = trans(x), j = i; i != nxt[x]; ++cnt, i = trans(pre[i])) { // 双指针找接近 n/2 的方案
while (j != nxt[x] && dis(pre[i], j) > n >> 1) j = trans(pre[j]);
check(dis(pre[i], j));
check(dis(pre[i], nxt[snart(j)]));
}
::cnt += 1ll * cnt * (cnt + 1);
} int stack[N], top;
inline void init() {
memset(lst, 0, sizeof (int) * (k + 1)), top = 0;
for (int i = 1; i <= n; ++i) nxt[i] = 0, nxt[lst[val[i]]] = i, lst[val[i]] = i;
for (int i = 1; i <= n; ++i) if (nxt[i]) {
while (top && nxt[stack[top]] <= i) --top;
while (top && nxt[stack[top]] <= nxt[i]) merge(val[i], val[stack[top--]]);
stack[++top] = i;
} // 单调栈操作
for (int i = 1; i <= n; ++i) val[i] = get(val[i]); // 直接设置成合并后的颜色
} signed main() {
fread(buf, 1, MAX, stdin);
read(n), read(k);
for (int i = 1; i <= n; ++i) read(val[i]);
for (int i = 1; i <= k; ++i) fa[i] = i;
init(), init(); // 预处理两遍
memset(lst, 0, sizeof (int) * (k + 1));
for (int i = 1; i <= n; ++i) {
if (!fir[val[i]]) fir[val[i]] = i;
else nxt[lst[val[i]]] = i;
lst[val[i]] = i;
}
for (int i = 1; i <= n; ++i) {
if (!nxt[i]) nxt[i] = fir[val[i]];
pre[nxt[i]] = i;
} // 处理前驱后继
int i = 1; do solve(i), i = nxt[i]; while (i != 1); // 最外层调用
printf("%lld %d", cnt >> 1, ans);
return 0;
}

后记

希望本文能够帮助你熟悉每一种方法。也希望我以后遇到这种题别寄了啊!

[POI2015] POD 题解的更多相关文章

  1. Luogu3587[POI2015]POD - hash + 单调队列

    Solution 还是去看了题解. 感谢大佬的博客→  题解传送门 是一道思路比较新的题. 搞一个前缀和, 记录前 $i$ 个位置每种颜色的出现次数, 如果位置 $i$ 是 颜色 $a[i]$ 的最后 ...

  2. BZOJ3747:[POI2015]Kinoman——题解

    https://www.lydsy.com/JudgeOnline/problem.php?id=3747 https://www.luogu.org/problemnew/show/P3582 共有 ...

  3. P3587 [POI2015]POD

    题目描述 长度为n的一串项链,每颗珠子是k种颜色之一. 第i颗与第i-1,i+1颗珠子相邻,第n颗与第1颗也相邻.切两刀,把项链断成两条链.要求每种颜色的珠子只能出现在其中一条链中.求方案数量(保证至 ...

  4. POI2015 解题报告

    由于博主没有BZOJ权限号, 是在洛咕做的题~ 完成了13题(虽然有一半难题都是看题解的QAQ)剩下的题咕咕咕~~ Luogu3585 [POI2015]PIE Solution 模拟, 按顺序搜索, ...

  5. luogu P3592 [POI2015]MYJ

    题目链接 luogu P3592 [POI2015]MYJ 题解 区间dp 设f[l][r][k]表示区间l到r内最小值>=k的最大收益 枚举为k的位置p,那么包含p的区间答案全部是k 设h[i ...

  6. CF786B Legacy(线段树优化建边)

    模板题CF786B Legacy 先说算法 如果需要有n个点需要建图 给m个需要建边的信息,从单点(或区间内所有点)向一区间所有点连边 如果暴力建图复杂度\(mn^2\) 以单点连向区间为例,在n个点 ...

  7. POI2015题解

    POI2015题解 吐槽一下为什么POI2015开始就成了破烂波兰文题目名了啊... 咕了一道3748没写打表题没什么意思,还剩\(BZOJ\)上的\(14\)道题. [BZOJ3746][POI20 ...

  8. 【题解】PIE [POI2015] [P3585]

    [题解]\(PIE\) \([POI2015]\) \([P3585]\) 逼自己每天一道模拟题 传送门:\(PIE\) \([POI2015]\) \([P3585]\) [题目描述] 一张 \(n ...

  9. Bzoj 3747: [POI2015]Kinoman 线段树

    3747: [POI2015]Kinoman Time Limit: 60 Sec  Memory Limit: 128 MBSubmit: 553  Solved: 222[Submit][Stat ...

  10. [Poi2015]

    [POI2015]Łasuchy 一看以为是sb题 简单来说就是每个人获得热量要尽量多 不能找别人 首先这道题好像我自己找不到NIE的情况 很容易想到一个优化 如果一个数/2>另一个数 那么一定 ...

随机推荐

  1. Linux Debian安装教程

    Debian 是一个免费的开源操作系统,是最古老的 Linux 发行版之一,于 1993 年由 Ian Murdock 创建.它采用了自由软件协议,并且由志愿者社区维护和支持.Debian 的目标是创 ...

  2. 开启IPV6网络

    1.路由器开启IPV6支持 2.路由器关闭IPV6 DHCP服务 3.若外网无法访问则关闭路由器防火墙 或关闭系统防火墙 4.tomcat需设置地址为0.0.0.0

  3. EasyExcel 无法读取图片?用poi写了一个工具类

    在平时的开发中,经常要开发 Excel 的导入导出功能.一般使用 poi 或者 EasyExcel 开发,使用 poi 做 excel 比较复杂,大部分开发都会使用 EasyExcel 因为一行代码就 ...

  4. 关于c指针的理解

    1 #include<stdio.h> 2 { 3 int a= 100,b=10; 4 int *p1=&a,*p2=&b; 5 *p1=b; 6 *p2=a; 7 pr ...

  5. 基于 Termux 和 ipv6 把手机打造成公网服务器

    Termux 安装与配置 安装 从这下载: https://f-droid.org/en/packages/com.termux/ 初始化 授权读写手机储存 termux-setup-storage ...

  6. Taro 滚动切换tab页

    import React, { Component } from 'react' import { View, Text, ScrollView } from '@tarojs/components' ...

  7. IdentityServer4 获取发现文档 提示 Keyset is missing

    客户端请求提示 Keyset is missing 解决办法 添加以下代码 services.AddIdentityServer(options => { options.Events.Rais ...

  8. 面试官:Java线程可以无限创建吗?

    哈喽,大家好,我是世杰. 本次给大家介绍一下操作系统线程和Java的线程以及二者的关联 1. 面试连环call Java线程可以无限创建吗? Java线程和操作系统线程有什么关联? 操作系统为什么要区 ...

  9. C#开发单实例应用程序并响应后续进程启动参数

    C#默认的WinForm模板是不支持设置单实例的,也没有隔壁大哥VB.NET那样有个"生成单个实例应用程序"的勾选选项(VB某些时候要比C#更方便),实现单实例可以有多种方法: 检 ...

  10. Simple WPF: WPF 自定义按钮外形

    最新内容优先发布于个人博客:小虎技术分享站,随后逐步搬运到博客园. WPF的按钮提供了Template模板,可以通过修改Template模板中的内容对按钮的样式进行自定义,完整代码Github自取. ...