快乐的爆10周末(虽然已经过去了许久)
之前的博客全部都是在CSDN上用富文本编辑器写的,唯一一篇用Mardown写的源代码还被吃了,只好在这里放链接了
2022.4.30 五一假期快乐的第一天
2022.5.1 五一假期悲伤的第二天
2022.5.2 五一假期痛苦的第三天
浅谈无旋Treap (基于洛谷P3391 文艺平衡树)
当然这篇在CSDN上也有发,建议到CSDN食用QwQ
书接上回,终于还是把那道题改出来了,于是时隔多年我来把那天的题来写一写。
这一道题写了这么久,果然还是太逊了。
书归正传,还是先来看一下题:
T1 匹配 Match 题目描述
给定一个仅含小写字母的字符串S[0..n-1],对于一个询问(p, q, len),我们想知道它的两个子串S[p..p+len-1]、S[q..q+len-1] 是否相同。更多地,我们希望在对串S 完成一些操作之后还能高效地得到这个结果。我们具体要维护以下几个操作(其中L 为操作之前的串长):
●1 pc:在下标p之前插入一个小写字母c,0<=p<=L,p=0意味在头插入,p=L意味在尾插入;
●2 p: 删除S[p],其中0<=p<L;
●3 p q:将S[p..q] 翻转为S[q..p],例如batta经过“3 1 3” 之后为bttaa;
●4 p q len: 表示一次询问操作,询问S[p..p+len-1] 与S[q..q+len-1] 是否相同,其中0<=p<=p+len-1<L,0<=q<=q+len-1<L。输入格式
第1行为两个整数n, m,分别表示字符串的初始长度和操作次数;第2 行为一个长度为n 的字符串;接下来m 行,每行为一个操作。
输出格式
仅一行一个整数,表示询问相同的次数。
样例输入
4 4
aacb
2 2 // aab
1 2 b // aabb
3 1 2 // abab
4 0 2 2 // ab == ab样例输出
1
数据范围与约定
对于前20%的数据满足n,m≤1000;
对于前70%的数据满足仅含有3、4两种操作;
对于100%的数据满足1≤n, m≤200000。
分析
对吧,这个题一看就是一道Splay的板子题,只是很可惜,这个人实在是太菜了,根本他就写不来Splay,所以显然我们再次选用了范浩强树,也就是无旋Treap,至于怎么做嘛,建议去看看我上一篇博客.
那么现在我们来真正讨论一下这道题。
显然我们直接判断字符串相等是不太现实的 (不过我们测试的时候时限开的5s,直接用String也能过) 我就不做赘述了,详情还是去看杰哥的博客 ,他把那篇码粘了上去。
于是乎我们想到用哈希来维护这个性质,我们每个节点上储存一个字符,再维护一个以这个节点为根的子树所构成的字符串的正向哈希(hash_for)和一个反向哈希(hash_op)每当执行Reverse操作的时候,我们只需要找到那个根节点,给他打上一个标记(tag),表示这个节点为根的子树经历了翻转操作,但是我还没有对其子树进行调整,知道我们再一次需要调用这棵子树的时候我们再去调整其子树,也就是PushDown操作(这里可以类比于线段树的延迟标记(tag)或者说是懒标记(lazy))。
部分代码如下:
void Reverse(int l, int r){
int x, y, z;
SplitBySize(rt, l - 1, x, y);
SplitBySize(y, r - l + 1, y, z);
tr[y].tag ^= 1;
rt = Merge(Merge(x, y), z);
}
那么我们怎么去PushDown呢?
当我们扫描到一个节点并发现他被打上了tag标记,这就说明这棵子树被翻转了,那么我们将他的左右子树交换,并把这个根节点上的正向哈希和反向哈希交换。我们把根节点上的tag消除,并为他的子节点的tag做出调整。至于为什么嘛,你自己想一想就知道了。
部分代码如下:
void PushDown(int rt){
if(tr[rt].tag){
swap(tr[rt].hash_for, tr[rt].hash_op);
swap(tr[rt].Hash_for, tr[rt].Hash_op);
swap(tr[rt].ls, tr[rt].rs);
if(tr[rt].ls) tr[tr[rt].ls].tag ^= 1;
if(tr[rt].rs) tr[tr[rt].rs].tag ^= 1;
tr[rt].tag = 0;
}
}
说到这里,你一定又要问了,哎呀哎呀,你到现在还没有说到底怎么维护哈希??!!
哎,别急嘛,这不马上就来了。
我们考虑我们最常用的哈希函数建立,就是将字符串转换为一个k进制数,我们记他为BASE进制,那么,我们找到一个根节点,他的左子树是根节点字母的左边部分,右子树是其右边部分,于是联想到我们哈希函数的定义,这个根上的正向哈希值就应当是 注意,我们这里乘的pow一定是右子树的大小而不是左子树,以为我们的左子树是高位,而右子树的低位(当然据杰哥所说反着写也可以)(这里的pow是指BASE的整数次方)。自然,我们的反向哈希就是。我们只需要在Update操作中在更新子树大小的同时,用上述方法更新我们的哈希值即可。
但是这里一定要注意,因为我们需要调用其子树上的哈希值,因此我们需要对他的左右子树先PushDown再更新(别问我怎么知道的,你猜我为什么调了这么多天)。
部分代码如下:
void Update(int rt){
PushDown(rt);
PushDown(tr[rt].ls);
PushDown(tr[rt].rs);
tr[rt].siz = tr[tr[rt].ls].siz + tr[tr[rt].rs].siz + 1;
tr[rt].hash_for = tr[tr[rt].ls].hash_for * pw[tr[tr[rt].rs].siz + 1] + tr[rt].val * pw[tr[tr[rt].rs].siz] + tr[tr[rt].rs].hash_for;
tr[rt].hash_op = tr[tr[rt].ls].hash_op + tr[rt].val * pw[tr[tr[rt].ls].siz] + tr[tr[rt].rs].hash_op * pw[tr[tr[rt].ls].siz + 1];
tr[rt].Hash_for = tr[tr[rt].ls].Hash_for * pww[tr[tr[rt].rs].siz + 1] + tr[rt].val * pww[tr[tr[rt].rs].siz] + tr[tr[rt].rs].Hash_for;
tr[rt].Hash_op = tr[tr[rt].ls].Hash_op + tr[rt].val * pww[tr[tr[rt].ls].siz] + tr[tr[rt].rs].Hash_op * pww[tr[tr[rt].ls].siz + 1];
}
这里我采用了双哈希(因为刚开始我以为是冲突的问题,我还改了好几遍BASE),实际上我们只需要一个BASE即可,这里这道题我们使用unsigned long long自然溢出也可以过。
最后我们怎么来判断这两个部分是否一样呢?我们只需要根据位置将其使用Split操作将中间那部分分离出来即可。(记得分离完一定要给他Merge回去,别猜了,这个我没错)
部分代码如下:
bool Check(int p, int q, int len){
int x = 0, y = 0, z = 0;
SplitBySize(rt, p + len - 1, x, z);
SplitBySize(x, p - 1, x, y);
int h1 = tr[y].hash_for, h3 = tr[y].Hash_for;
Merge(Merge(x, y), z);
x = 0, y = 0, z = 0;
SplitBySize(rt, q + len - 1, x, z);
SplitBySize(x, q - 1, x, y);
int h2 = tr[y].hash_for, h4 = tr[y].Hash_for;
Merge(Merge(x, y), z);
if(h1 == h2 && h3 == h4 ){
return true;
}else{
return false;
}
}
然后,这道题我们就完美解决了,这个的时间甚至超过了标程封装优化了许多的Splay还要快。
代码
下面就是完整代码时间了:
//省略头文件和快读
const int BASE = 13331;
const int BAS = 233;//当时搞得第二个模数,实际上可以忽略
int n, m;
char s[MAXN];
ull pw[MAXN], pww[MAXN];
void init()
{
pw[0] = pww[0] = 1;
for(int i = 1; i <= n + m; i++){
pw[i] = pw[i - 1] * BASE;
pww[i] = pww[i - 1] * BAS;
}
}
struct Point{
int ls, rs;
int siz;
int val, dat;
int tag;
ull hash_for, hash_op;
ull Hash_for, Hash_op;
};
mt19937 rd(60505);
int rt;
struct SplitTreap{//膜拜FHQ大佬orzzzzzzzzzzzzzzzzzz(千足虫(大雾))
Point tr[MAXN];
int id;
void Update(int rt){
PushDown(rt);
PushDown(tr[rt].ls);
PushDown(tr[rt].rs);
tr[rt].siz = tr[tr[rt].ls].siz + tr[tr[rt].rs].siz + 1;
tr[rt].hash_for = tr[tr[rt].ls].hash_for * pw[tr[tr[rt].rs].siz + 1] + tr[rt].val * pw[tr[tr[rt].rs].siz] + tr[tr[rt].rs].hash_for;
tr[rt].hash_op = tr[tr[rt].ls].hash_op + tr[rt].val * pw[tr[tr[rt].ls].siz] + tr[tr[rt].rs].hash_op * pw[tr[tr[rt].ls].siz + 1];
tr[rt].Hash_for = tr[tr[rt].ls].Hash_for * pww[tr[tr[rt].rs].siz + 1] + tr[rt].val * pww[tr[tr[rt].rs].siz] + tr[tr[rt].rs].Hash_for;
tr[rt].Hash_op = tr[tr[rt].ls].Hash_op + tr[rt].val * pww[tr[tr[rt].ls].siz] + tr[tr[rt].rs].Hash_op * pww[tr[tr[rt].ls].siz + 1];
}
void PushDown(int rt){
if(tr[rt].tag){
swap(tr[rt].hash_for, tr[rt].hash_op);
swap(tr[rt].Hash_for, tr[rt].Hash_op);
swap(tr[rt].ls, tr[rt].rs);
if(tr[rt].ls) tr[tr[rt].ls].tag ^= 1;
if(tr[rt].rs) tr[tr[rt].rs].tag ^= 1;
tr[rt].tag = 0;
}
}
void SplitBySize(int rt, int rk, int &l, int &r){
if(!rt){
l = r = 0;
return ;
}
PushDown(rt);
if(rk >= tr[tr[rt].ls].siz + 1){
l = rt;
SplitBySize(tr[rt].rs, rk - (tr[tr[rt].ls].siz + 1), tr[rt].rs, r);
}else{
r = rt;
SplitBySize(tr[rt].ls, rk, l, tr[rt].ls);
}
Update(rt);
}
int Merge(int l, int r){
if(!l || !r) return l | r;
int rt;
if(tr[l].dat <= tr[r].dat){
PushDown(l);
rt = l;
tr[rt].rs = Merge(tr[l].rs, r);
}else{
PushDown(r);
rt = r;
tr[rt].ls = Merge(l, tr[r].ls);
}
Update(rt);
return rt;
}
int NewPoint(char v){
++id;
tr[id].ls = tr[id].rs = 0;
tr[id].val = v;
tr[id].hash_for = v;
tr[id].hash_op = v;
tr[id].Hash_for = v;
tr[id].Hash_op = v;
tr[id].dat = rd();
tr[id].siz = 1;
return id;
}
void InsertByRank(int &rt, int rk, char ch){
int x = 0, y = 0;
SplitBySize(rt, rk, x, y);
x = Merge(x, NewPoint(ch));
rt = Merge(x, y);
}
void EraseByRank(int &rt, int rk){
int x = 0, y = 0, z = 0;
SplitBySize(rt, rk, x, z);
SplitBySize(x, rk - 1, x, y);
rt = Merge(x, z);
}
void Reverse(int l, int r){
int x, y, z;
SplitBySize(rt, l - 1, x, y);
SplitBySize(y, r - l + 1, y, z);
tr[y].tag ^= 1;
rt = Merge(Merge(x, y), z);
}
void Print(int rt){
if(!rt) return ;
PushDown(rt);
Print(tr[rt].ls);
printf("%c", tr[rt].val);
Print(tr[rt].rs);
}
bool Check(int p, int q, int len){
int x = 0, y = 0, z = 0;
SplitBySize(rt, p + len - 1, x, z);
SplitBySize(x, p - 1, x, y);
int h1 = tr[y].hash_for, h3 = tr[y].Hash_for;
Merge(Merge(x, y), z);
x = 0, y = 0, z = 0;
SplitBySize(rt, q + len - 1, x, z);
SplitBySize(x, q - 1, x, y);
int h2 = tr[y].hash_for, h4 = tr[y].Hash_for;
Merge(Merge(x, y), z);
if(h1 == h2 && h3 == h4 ){
return true;
}else{
return false;
}
}
}STP;
int ans = 0;
int main()
{
freopen("match.in", "r", stdin);
freopen("match.out", "w", stdout);
n = inpt(), m = inpt();
init();
scanf("%s", s + 1);
rt = 0;
for(int i = 1; i <= n; i++){
STP.InsertByRank(rt, i, s[i]);
}
for(int i = 1; i <= m; i++){
int flag = inpt();
if(flag == 1){
int p = inpt();
char ch[3];
scanf("%s", ch + 1);
STP.InsertByRank(rt, p, ch[1]);//根据题的定义(0~n-1),这里刚好可以直接用p
continue;
}
if(flag == 2){
int p = inpt();
STP.EraseByRank(rt, p + 1);
continue;
}
if(flag == 3){
int p = inpt(), q = inpt();
STP.Reverse(p + 1, q + 1);
continue;
}
if(flag == 4){
int p = inpt(), q = inpt();
int len = inpt();
if(STP.Check(p + 1, q + 1, len)){
ans++;
}
continue;
}
}
printf("%d", ans);
fclose(stdin);
fclose(stdout);
return 0;
}
好了,困扰了我好多天的第一题终于是搞定了,这两天差点没给我整吐了。
T2 渡河 River 题目描述
现在有N 名游客需要渡河到对岸,但是岛上只有两艘船,第一艘船可以容纳n1名游客,第二艘船可以容纳n2名游客,为了不浪费位置,船长要求每次必须坐满才能出发。现在已知使用第一艘船运输一趟需要花费c1,使用第二艘船运输一趟需要花费c2,问最小总花费。
输入格式
第一行为一个整数T,表示数据组数;接下来T行,每行五个整数N, n1, c1, n2, c2,含义如题意所述。
输出格式
输出T行对应T组数据的答案,每行输出两个整数m1和m2分别表示总花费最小时第一艘船和第二艘船的运输趟数,若无解则输出“No solution”。当有多组解时,优先取用第二艘船。
样例输入
2
43 3 1 4 2
40 9 5 12 5样例输出
13 1
No solution数据范围与约定
对于前30%的数据;
对于100%的数据,。
分析
这道题我们一看到题目,首先就可以列出一个式子,那么我们看到这个式子的时候自然就会想到裴蜀定理,用扩展欧几里得去解这样一个方程,而我们解出来的是,既然如此,那么我们发现时,这个问题就无解了,直接输出No solution即可。
接下来我们就可以解决花费最小的问题了,由我们小学学过的性价比的知识我们可以知道,再总价值一定的情况下,优先取性价比最高的那一个一定花费最小。那么也就是说,我们解出这个方程也是两个中其中一个取最多的时候有花费最小。(求最小正整数解的时候一定要注意,很容易写挂,别问我怎么知道的)。
我们只需要分别求x最小时的解,和y最小时的解就可以了,当然如果我们发现此时两种解中都存在x,y其中一个是负数的情况,那么自然也是无解的。
至于怎么去求最小整数解,可以去看看我的另一篇博客里的T1。
代码
对,这道题如此简短地我们就讲完了,下面来看代码:
//省略头文件和快读
int T;
int n, n1, c1, n2, c2;
int ex_gcd(int a, int b, int &x, int &y)
{
if(b == 0){
x = 1, y = 0;
return a;
}
int gcd = ex_gcd(b, a % b, x, y);
int t = x;
x = y;
y = t - y * (a / b);
return gcd;
}
int ans, ansx, ansy;
signed main()
{
freopen("river.in", "r", stdin);
freopen("river.out", "w", stdout);
T = inpt();
while(T--){
n = inpt();
n1 = inpt(), c1 = inpt();
n2 = inpt(), c2 = inpt();
int x, y;
int Gcd = ex_gcd(n1, n2, x, y);
if(n % Gcd){
puts("No solution");
continue;
}
int rat = n / Gcd;
int t1 = n2 / Gcd, t2 = n1 / Gcd;
x *= rat, y *= rat;
//求得某一个的最小整数解
if(x < 0){
int t = -x / t1;
if(x % t1 != 0){
t++;
}
x += t1 * t;
y -= t2 * t;
}
if(y < 0){
int t = -y / t2;
if(y % t2 != 0){
t++;
}
x -= t1 * t;
y += t2 * t;
}
if(x < 0 || y < 0){
puts("No solution");
continue;
}
//分别求出两种解
int LimD = -x / t1, LimU = y / t2;
int xMin = x + LimD * t1, yMin = y - LimD * t2;
int xMax = x + LimU * t1, yMax = y - LimU * t2;//这里的min,max只是象征意义上的min和max,只是表示两种情况
if(xMin * c1 + yMin * c2 <= xMax * c1 + yMax * c2){
printf("%lld %lld\n", xMin, yMin);
}else{
printf("%lld %lld\n", xMax, yMax);
}
}
fclose(stdin);
fclose(stdout);
return 0;
}
T3 奇回文串 Odd 题目描述
给定一个长为L的仅含小写字母的字符串,需要在其奇数长度的回文子串中找到最长的k个,并且输出它们长度的乘积对19961202取模的值,若奇数长度的回文子串不足k个则输出-1 。
输入格式
第一行两个整数L, k,如题意所述;
接下来一行为一个长为L的字符串。输出格式
输出一行一个整数,表示答案。
样例输入
5 3
ababa样例输出
45
样例解释
奇数长度的回文子串有:ababa, aba, aba, bab, a, a, a, b, b,最长的三个长度分别为5, 3, 3,故乘积为
数据范围与约定
对于前20%的数据,L, k≤100;
对于另外20%的数据,k=1;
对于100%的数据,。
分析
这道题是我觉得这三道题中最简单的一道(也是我考试的时候唯一拿了分的地方)。
这道题一看求回文串长度,自然就会想到Manacher。不会的同学可以去看看我收藏的这篇博客,多谢这位大佬唤起了我遗失多年的关于Manacher的回忆。
由于Manacher算法的定义,我们会发现只有在原本就是字母而不是我们填充字符的位置上的才有可能是奇数长度,于是我们只需要在Manacher的板子的基础上,记录下每一个原本就是字母的位置的回文串长度即可。
部分代码如下:
if(s[i] >= 'a' && s[i] <= 'z'){
q[i] = p[i] - 1;
f[q[i]]++;
mx = max(mx, q[i]);
}//这段代码孤零零的在这里可能看不懂,等会你看完整代码你就懂了
由于这道题的定义,我们的长回文串可以包含数个小回文串,而且允许重复,于是我们只需要开一个桶,记录每一个长度有多少个,然后将长的数目累加到短的数目上即可(代码中我是使用了一个tot变量记录累加的部分)。最终如果加完了都还没有达到k则输出-1。
至于最终结果嘛,我们只需要从大到小依次乘下去即可。
代码
摆烂啦讲完啦,下面来放代码:
//头文件和快读省略
int L, k;
char s[MAXL << 2];
int cnt = 0;
void init()
{
s[cnt] = '$';
s[++cnt] = '#';
char ch = getchar();
while(ch < 'a' || ch > 'z'){
ch = getchar();
}
while(ch >= 'a' && ch <= 'z'){
s[++cnt] = ch;
s[++cnt] = '#';
ch = getchar();
}
s[++cnt] = '^';
}
int p[MAXL << 2], q[MAXL];//回文半径 和 奇回文串长度
int f[MAXL], mx = 0xcfcfcfcf;//桶
void doit()
{
for(int i = 0; i <= cnt; i++){
p[i] = 1;
}
int r = 0, mid = 0;
for(int i = 0; i <= cnt; i++){
if(i + p[(mid << 1) - i] > r){
p[i] = r - i;
}else{
p[i] = p[(mid << 1) - i];
}
while(s[i - p[i]] == s[i + p[i]]){
p[i]++;
}
if(s[i] >= 'a' && s[i] <= 'z'){//记录奇回文串
q[i] = p[i] - 1;
f[q[i]]++;
mx = max(mx, q[i]);
}
if(p[i] + i > r){
mid = i;
r = i + p[i];
}
}
}
int qpow(int x, int y)
{
int val = 1;
while(y){
if(y & 1) val = 1ll * val * x % MOD;
x = 1ll * x * x % MOD;
y >>= 1;
}
return val;
}
int ans = 1;
int main()
{
freopen("odd.in", "r", stdin);
freopen("odd.out", "w", stdout);
L = inpt(), k = inpt();
//Manacher
init();
doit();
//累加和计算结果
int tot = 0;
for(int i = mx; i >= 1; i -= 2){
tot += f[i];
if(tot >= k){
ans = 1ll * ans * qpow(i, k) % MOD;
k -= tot;
break;
}
ans = 1ll * ans * qpow(i, tot) % MOD;
k -= tot;
}
if(k > 0){
puts("-1");
}else{
printf("%d", ans);
}
fclose(stdin);
fclose(stdout);
return 0;
}
实际上这道题我想复杂了,其实只需要不填充字符,求出来的自然就是奇回文串,只是需要修改一下我们的由回文半径转为回文串长度的计算方法,这里我就不写了,自己想一下很容易可以想到,实在想不到就用我这个也行。
这样快乐的一周就要过去了,我的周末作业至今都还没写完,我现在只想摆烂。。。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】