训练纪要壹
主要目的是记录每天做的题尤其是模拟赛题。
次要目的是记点日记,偶尔说点垃圾话,但是这部分或许会占篇幅,所以缩起来。
嗯应该定一个目标,就是 CSP 和 NOIP 考多少分多少名,但是还没想好,先研究研究自己实力。
集训了啊,最后一个赛季,立点 flag。
模拟赛题得改,以后每个题都要真的着手开始改,不能还没改就不改了,哪怕很不想改也得逼自己。
多思考理解理解本质,感觉已经没有总是返工去学没有学牢的知识的时间了,碰到就多研究多思考吧,好好理解本质,知识点和题都是。
多写博客,主要是用来辅助自己深度思考,其次可能需要多总结一些经验。
好好利用时间,高二的确没有心思颓了啊,但是时间利用率还是很低,主要可能总是走神,努力改一改。
豁达一点,少被一些不开心的事情影响状态。
9.22
如此状态,如何集训。
本来是明天集训的啊,结果晚上被“华”奥扫地出门了,直接提前开始。
打 ARC 已经不好评了,或许是太久没打 AT 了,成功做到调试时 ifdef->ifndef
没改回来、输出了 endl
但是关闭同步流。最重要的肯定是把交互返回的 0
1
看反。虽然一开始的确写假了但是后面硬控自己到 \(80\) min,还是涛哥说我才知道,甚至中途还换了一种写法。
如此状态,如何集训。
ARC184A
一种正常思路的做法是发现 \(950=1000\times \dfrac{19}{20}\),所以每 \(20\) 个分一组,判断 \(19\) 次分成两边,每组中选择少一点的那边。一组中有 \(10\) 个的话可以直接拉一个别的过来比一下,如果直到最后一组之前还没有找出任何一个,说明最后一组中有 \(10\) 个,但是最后在拉别的过来比就超一个,所以直接拿另外一个与其中 \(19\) 个比。
我的思路是每次合并两组,不断将一个一组变成两个一组,四个一组,八个一组,十六个一组。如果合并时候发现不一样就把他俩一起拿出来。等到最后所有十六个一组的就都是好的,所以拿一个出来用来检验,之前每次拿出来的两组中有一组全好一组全坏,所以复杂度上界大概是 \(500+250+125+62+10=947\)。
9.23
唐万乐
\(55+100+0+20\),rk8,唐万乐。
T1 切不掉,T3 计算几何,T4 狂写 2.5h 80pts 部分分发现题读错了,事实上写之前还写了发暴力来看看自己读题对不对,但是只测了小样例没测大样例。后来发现的确只需要改一改,不过感觉场上的确写不完了。
把 T4 80pts 调了,T1、T3 改了,T4 正解没写啊,感觉有点困难了。
怎么一天就过去了?晚上写完博客效率还是低啊,抓紧点。
CSP-3
A.奇观
当 \(x\) 作为 C
中的 ⑧ 号点,相当于选出以 \(x\) 开头的一条边数为 \(1\) 的链和一条边数为 \(2\) 的链,作为 F
中的 ② 号点就是两条边数为 \(1\) 的链和一条边数为 \(2\) 的链,处理个 \({d_1}_i=\sum_j [(i,j)\in E],{d_2}_i=\sum_j {d_1}_j[(i,j)\in E]\) 就做完了。
怎么评价这个做不出。
B.铁路
转化成路径标记,全局数未标记边个数。并查集维护连通块,直接跳到未标记的边下面标记,LCA
事实上都不用求了。
C.光纤
有结论是这条线一定平行于所有圆心的凸包上的一条边,大概可以调整法证明,我会了但是我不在这写。
然后就相当于求凸包上每条边与最远点距离的最小值的一半,直接旋转卡壳。
D.权值
询问给的很迷惑,可以转化成把所有值为正的极长连续段 \([l_i,r_i]\) 拿出来,设总共有 \(c\) 个,每个连续段的权值 \(w_i=(r_i-l_i+1)\prod\limits_{j=l_i}^{r_i} a_j\),求 \(\sum\limits_{i=1}^{c} i(c-i+1)w_i\)。
将一个数拆成绝对值和符号分别维护,修改操作分别等价要实现区间赋值,区间符号赋值,区间符号取反,区间绝对值赋值。
先不考虑操作 \(4\),那么线段树上区间维护三种 tag,还有当前答案与区间符号翻转后的答案与忽略符号的答案。
合并答案实际上没什么特殊的,唯一可能会想不到的是维护区间每个连续段 \(i\) 的 \(a_i\times i\) 与 \(a_i\times (cnt-i+1)\)(\(i\) 和 \(cnt\) 指的是区间中所有已经合并完,贡献到答案的连续段中的第 \(i\) 个,且总共有 \(cnt\) 个),直接挂个 push_up
吧,不想说了。
点击查看代码
struct U{unsigned ans,num,numl,numr,sl,sr;int cl,cr,cnt;bool B;};
//t[p].B 是判断 p 表示的区间是否是一整个连续段
U I={0,0,0,0,1,1,0,0,0,0};
struct node{int len,a,b,lz;U A,B,C;}t[MAXN<<2];
inline U merge(U x,U y)
{
unsigned ans=x.ans+y.ans+x.numl*y.cnt+y.numr*x.cnt,num=x.num+y.num;
unsigned numl=x.numl+y.numl+y.num*x.cnt,numr=x.num*y.cnt+x.numr+y.numr;int cnt=x.cnt+y.cnt;
if(x.B&&y.B) return {ans,num,numl,numr,x.sl*y.sl,1,x.cl+y.cl,0,cnt,1};
if(x.B) return {ans,num,numl,numr,x.sl*y.sl,y.sr,x.cl+y.cl,y.cr,cnt,0};
if(y.B) return {ans,num,numl,numr,x.sl,x.sr*y.sl,x.cl,x.cr+y.cl,cnt,0};
if(x.cr||y.cl)
{
unsigned now=x.sr*y.sl*(x.cr+y.cl);++cnt;
ans+=x.numl+y.numr+now*(x.cnt+1)*(y.cnt+1),num+=now;
numl+=y.num+now*(x.cnt+1),numr+=x.num+now*(y.cnt+1);
}
return {ans,num,numl,numr,x.sl,y.sr,x.cl,y.cr,cnt,0};
}
inline void push_up(int p)
{
t[p].A=merge(t[p<<1].A,t[p<<1|1].A);
t[p].B=merge(t[p<<1].B,t[p<<1|1].B);
t[p].C=merge(t[p<<1].C,t[p<<1|1].C);
}
至于操作 \(4\),长度为 \(len\) 的区间中最多有 \(O(\sqrt n)\) 种长度不同的正连续段,对每个长度维护一下那个 \(i(cnt-i+1)\) 的系数和,就可以做了。
这样合并的复杂度是当前区间长度的根号,所以实际上一次操作变为 \(O(\sqrt n)\) 而不是 \(O(\sqrt n\log n)\)。
不过快速幂那里还有一个 \(\log\)。
9.24
烂
现在每天的日程变成了 跑操 + 早读 + 模拟赛 + 一周两次的体育课。感觉把模拟赛类比一下的话就跟体特一个情况了。
模拟赛 \(100+20+40+0\),rk5,完蛋,挂了 140pts。
你要是不会用位运算就别瞎用了,想判断 \(i\) 的第一二进制位和 \(j\) 的第零二进制位是否相同,直接写 !((i&2)^(j&1))
挂了 80pts。
你怎么他妈不会字符串不会多项式不会数论到现在连流都一点不会了?你怎么把一种边建反了啊?你怎么又挂了 60pts?
还有你那个狗屎开题策略,为什么在对前三个题都有思路的情况下去瞎写 T4?你真的猜不到 T4 是你一点都不会的生成函数吗?对啊你猜到了为什么要去写啊?
还有你为什么分别在前两道题上读错三次?导致两小时时得分还是零?
你改题更是唐完了,先是对着 T2 瞪了俩小时模了一堆东西才看出来。然后因为太急 T3 狂改各种写法,结果你他妈不是好好想想就知道只需要调换两个字符就过了吗?
所以呢,你一上午挂 140pts,一下午对字符的有效编辑不超过 \(10\) 个。然后一天就过去啦!
我求求你别这么打模拟赛了,起码别读错题了行不行?你现在打的还不如去年呢再这么打抓紧退了吧。
晚上收拾半天把书拿过来了,因为他说可能后面还要穿插文化课?我并不反对但是觉得挺奇怪。
我急了,今天的之后补。
可能永远不会补。
CSP-4
A.商品
B.价值
C.货币
D.资本
9.25
今天打的还行啊
今天感觉时间没有浪费啊,\(100+100+20+35\),rk1。
不过发现现在一改题就一下午,所以以后题解可能会写的短。
CSP联-1
A.几何
发现暴力 DP 复杂度 \(1e9\),如果除个 \(w\) 就过了,然后你 bitset 优化一下,注意处理一些细节。
B.分析
等会有点假。
\(\huge{假了}\)
大概原因是子树转移顺序有影响。。、
怒了。
发现 A 操作相当于经过一条边的第一次不需要代价,之后每次都是 \(A\)。
直接设 \(f_{x,0/1,0/1}\) 表示 \(x\) 子树中,从 \(x\) 或任意点出发,到 \(x\) 或任意点结束的答案。
然后合并的时候相当于合并两条操作路径,从 \(f_{x,i,j}\) 和 \(f_{y,p,q}\) 合并(一个在前一个在后两种情况),中间可能会经过 \(x\) 与 \(y\) 间的边,如果合并完的新路径开始或者结束是 \(y\) 的话可以从 \(x\) 或向 \(x\) 走,需要保证每次转移都至少经过了一次 \(x\) 与 \(y\) 间的边。
然后每次更新完了,在两维中都可以分别用 \(B\) 的代价将一个 \(1\) 修改为 \(0\),这么写可能方便多。
下午讲的时候有一种更新其实是不必要的,挂个代码自己看吧,我觉得挺好看的。
点击查看代码
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<string.h>
#define ll long long
using namespace std;
const int MAXN=5e5+10;
const ll INF=1e17;
int n;ll f[MAXN][2][2],A,B;
vector <int> v[MAXN];
void dfs(int x,int fa=0)
{
for(int y:v[x])
{
if(y==fa) continue;dfs(y,x);
ll g[2][2]={INF,INF,INF,INF};
for(int i:{0,1}) for(int j:{0,1}) for(int p:{0,1}) for(int q:{0,1})
{
int cnt=(j==0&&p==0);
if(cnt) g[i][1]=min(g[i][1],f[x][i][j]+f[y][p][q]);
// if(i==0) g[1][1]=min(g[1][1],cnt*A+f[x][i][j]+(j|p)*B+f[y][p][q]);
if(q==0) g[i][0]=min(g[i][0],f[x][i][j]+(j|p)*B+f[y][p][q]+cnt*A);
}
for(int p:{0,1}) for(int q:{0,1}) for(int i:{0,1}) for(int j:{0,1})
{
int cnt=(q==0&&i==0);
if(cnt) g[1][j]=min(g[1][j],f[y][p][q]+f[x][i][j]);
if(p==0) g[0][j]=min(g[0][j],cnt*A+f[y][p][q]+(q|i)*B+f[x][i][j]);
// if(j==0) g[1][1]=min(g[1][1],f[y][p][q]+(q|i)*B+f[x][i][j]+cnt*A);
}
f[x][0][0]=min({g[0][0],g[1][0]+B,g[0][1]+B,g[1][1]+B+B});
f[x][0][1]=min(g[0][1],g[1][1]+B);
f[x][1][0]=min(g[1][0],g[1][1]+B);
f[x][1][1]=g[1][1];
}
}
signed main()
{
freopen("analyse.in","r",stdin);
freopen("analyse.out","w",stdout);
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>A>>B;A=min(A,B);
for(int i=1,x,y;i<n;++i)
cin>>x>>y,v[x].push_back(y),v[y].push_back(x);
dfs(1),cout<<min({f[1][0][0],f[1][0][1],f[1][1][0],f[1][1][1]})<<'\n';return 0;
}
C.代数
感觉是个很好的题啊。首先原问题相当于求对于每个点 \(u\),选出 \(k\) 个可重点使得均在 \(u\) 子树中的概率和。
感觉这步很牛,考虑在后面加 \(k\) 个点 \([n+1,n+k]\),使这些点的父亲在 \([1,n]\) 中,也就是他们的父亲就是选出来那 \(k\) 个点。
设 (我懒了我懒了)
D.组合
改了,但是感觉无意义,不说了。本来想写部分分,但是感觉不是很好说,也不说了。
感觉刚开始几天就已经开始摆博客了,所以我们先忽略前面。
9.26
没有模拟赛效率低下啊!
CF708D Incorrect Flow
有趣题!网络流真是常做常新啊!
最小费用最大流。
给你一个流的方案,但是不一定对,可以修改边的容量和流量,要用最小的代价将流改对。
首先感觉很难转化成流以外的东西吧,所以先套路的转化。
类似上下界网络流中,建一个超级源和超级汇,然后若一个点的流出比流入多就从超级源连一条容量是差值,代价是 \(0\) 的边,反之反之。
然后我居然忘记了可以连一条 \(n\to 1\) 容量为 \(\infty\) 代价是 \(0\) 的边来取消本来的源汇,差这一步就自己做完了,可能因为很久没做上下界了。
然后边的话有两种情况。
当 \(f\leq c\) 的时候,\(u\to v\) 有一条 \((c-f,1)\) 的边,相当于改了流量,还有一条 \((\infty,2)\) 的边,相当于即改流量又改容量,\(v\to u\) 有一条 \((f,1)\) 的边,相当于改一个流量,把流过的流量退流。
当 \(f>c\) 的时候,显然我们不能有负流量,所以先将这条边的流量扩大到 \(f\),然后 \(u\to v\) 仍有一条 \((\infty,2)\) 的边,\(v\to u\) 有一条 \((f-c,0)\) 的边,相当于改一个流量,但是把改过的容量取消了,所以代价是 \(0\),还有一条 \((c,1)\) 的边,也是退流。
CF1146G Zoning Restrictions
多对一限制,考虑最小割。
直接将一个点每种决策穿起来,割哪条边就是建多高,然后将 \([l,r]\) 的第 \(x\) 层节点连到一个新建节点,边权为 \(\infty\),然后新点到汇点连 \(c\) 的边,也就是要是一个位置建的高度比这个大,就有流到这层的流,就还要割掉这条。
9.27
闲话
中午起床看到有一群演出的,仔细一看有个人穿着二中校服,再往旁边一看看到了初二同学。
一开始没敢认,后来看清楚了喊了一句,不过声音有点小,再想喊第二句她就已经走进门了。
后来想了很多啊,就想起来跟她坐同桌的时候,当时坐了快半年的同桌,感觉挺有意思的。
后来分班肯定就没什么交集了,只不过一见多少有点意外。
晚上吃完饭回去好像又看见了,不过感觉没什么必要很刻意的过去打招呼。
只不过感觉可能永远也见不到了啊,感觉很多人,之前可能很熟的人,可能连句再见也没说就再也见不到了。可能说了,但是没意识到再也见不到了。
进而比较怀念自己的初二那个班,除了几个瞎装逼的其他人都是非常好的啊,当时在班里呆的不多不少,不像现在基本天天在机房,连班里人都不熟。
小学同学也是,可能小学还比较内向,尽管六年没分班可能跟有的人也超不过十句话。
倒不是说要跟每个人都很熟,跟每个人都保持一生的交情,只是萍水相逢一场,稀里糊涂就各奔东西了,感觉有点遗憾。
这场简单点啊,因为不是正睿吗?
\(100+100+100+75(55)\),T4 数据水多了 20pts,不过 rk1。
改完题就去做机器蚤分组,结果发现过于困难,就弃了。
然后开了到 CF666E,结果发现一点不会,然后看了题解,读懂了,但是很没有成就感,写了一半不想写了。效率低下啊。
然后随便去洛谷上开了到流,发现是 HBCPC,牛老师场上切不了的题,看了两眼放弃了,决定之后再看。
所以改完题啥也没干,不能总这么摆。
不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。不能总这么摆。
CSP-5
A.光
这题感觉有点质量,做法也多。
设 \(a,b,c,d\) 为耗电量,\(A,B,C,D\) 是要求的最低亮度。
枚举 \(x=a+d,y=b+c\),这样 \(a,d\) 对 \(c,d\) 的贡献是 \(\lfloor\dfrac{a}{2}\rfloor+\lfloor\dfrac{d}{2}\rfloor\),根据奇偶贡献会是 \(\lfloor\dfrac{x}{2}\rfloor\) 或 \(\dfrac{x}{2}-1\),所以可以分别枚举 \(a,b\) 的奇偶性。
然后 \(A,D\) 和 \(B,C\) 之间就没有影响了,对 \(A\) 的限制是 \(a+\lfloor\dfrac{x-a}{4}\rfloor\geq A-(b,c\text{ 对 A 的贡献})\),把 \(a\) 解出来,带到 \(D\) 上看看是否合法,另一组同理。
B.爬
一个点的贡献基本只与它和其儿子有关,考虑对每个点的贡献按位考虑。
在 \(x\) 点,设 \(s=|son(x)|+1\),\(c_i\) 是 \(x\) 及其儿子中第 \(i\) 位为 \(1\) 的个数,则 \(x\) 点对答案的贡献为:
当 \(x=1\) 的时候,由于它并不能向上,所以会有 Corner Case,不写了,一个意思。
C.字符串
不太理解这题为啥放 T3。
直接贪心,枚举有 \(i\) 段 B,那么有 \(j\) 段 A,有 \(k\) 段 B 后有 A(\((j,k)\in\{(i-1,i-1),(i,i-1),(i,i),(i+1,i)\}\))。
这样那 \(k\) 段先放 \(c\) 个 B,其它段放 \(1\) 个 A 或 B,然后剩下的 A 每 \(a\) 个有一个贡献,剩下的 B 先尽可能将长度为 \(c\) 的 B 段补齐,然后每 \(b\) 个有一个贡献。
D.奇怪的函数
发现操作要不就是将函数图像沿 \(y\) 轴平移,要不就是拿一条直线 \(y=c\) 截,所以函数会是一个最多三段的分段函数。
然后直接线段树或分块维护。
9.28
\(100+100+100+10\),rk2,前三题那么水怎么后面放个这。
晚上 ABC 打到 G 摆了,感觉这种思路确实很难想,没怎么见过。
不过感觉会刚好卡到 1 Dan 下面,控控分多上一点吧,要不然上去以后也不涨分也不打 ABC,好不容易来场 ARC 就下了。
CSP-6
A.一般图最小匹配
排序,只会选相邻,\(O(n^2)\) DP。
B.重定向
贪心,分情况讨论,在还没有删过数的时候:
-
\(a_i = 0\),若现在没有填的数中最小的被后面占用了,把后面那个删了放这。
-
\(a_i\ne 0,a_{i+1}\ne 0\),若 \(a_{i+1}<a_i\),把 \(i\) 删了。
-
\(a_i\ne 0,a_{i+1}=0\),若现在没有填且没有被占用的数中最小的比 \(a_i\) 小,就把 \(i\) 删了,这样 \(a_{i+1}\) 填完比 \(a_i\) 小。
-
\(i=n\),删了。
C.斯坦纳树
树上一个点集的斯坦纳树是点集的虚树。
把全 \(0\) 连通块缩起来。
所以新加入一个点的时候往包含之前所有点的虚树走,走到第一个走过的位置就是一个新增的 LCA,然后这些点都得在现在选择过的点集中。
所以把点集第一个点当根,每次暴力往上走,每个点只会被走一次。
D.直径
我猜不会有人看,而且感觉不好写,直接放代码吧。
先考虑取出直径中点(中边)当做根,然后相当于将若干子树挂到点上,每个子树有用的信息是子树大小,子树深度,子树中深度最大的点(称为关键叶子)的个数。
所以设 \(f_{d,i,j}\),为子树深度为 \(d\),有 \(i\) 个节点,其中 \(j\) 个深度为 \(d\),的无标号有根树方案。
这个东西直接从所有 \(d'<d-1\) 中枚举子树大小(前缀和优化一下),从 \(d'=d-1\) 中枚举子树大小,关键叶子个数。
转移的时候同一种子树排列会有重复贡献,所以枚举选了几种,每种选了几个,类似背包的更新。
如果直径长度是偶数,最后往中点上挂的时候还要再记一下目前有多少条直径,就是有多少对在不同的深度为 \(\dfrac{k}{2}-1\) 的子树中的关键叶子对。
如果直径长度是奇数,说明只能分成深度为 \(\lfloor\dfrac{k}{2}\rfloor\) 的两个子树,直接枚举一个的子树大小和关键叶子个数,另一个也就知道了,别统计重就行。
循环套了 7 层,但其中一层是调和级数,不过一层套一层越来越不满,反正复杂度很对。
点击查看代码
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<string.h>
#define int long long
#define ll long long
using namespace std;
const int N=45,mod=998244353;
ll n,K,p,F[N][N][N],G[N][N][N][N],S[N],I[N];
inline void A(ll &x,ll y)
{x+=y;if(x>=mod) x-=mod;}
inline ll ksm(ll a,int b=mod-2)
{
ll ans=1;for(;b;b>>=1,a=a*a%mod)
if(b&1) ans=ans*a%mod;
return ans;
}
signed main()
{
freopen("dia.in","r",stdin);
freopen("dia.out","w",stdout);
cin>>n>>K>>p,F[1][1][1]=1;
for(int i=1;i<=n;++i) I[i]=ksm(i);
for(int d=2;d<=K/2+1;++d)//枚举深度
{
ll f[N][N][N];memset(f,0,sizeof(f));f[0][1][0]=1;
for(int k=1;k<n;++k)//枚举转移大小 (限制最大深度 d-2)
{
for(int c=1;c<=min(n/k,S[k]);++c)//枚举同一大小选几种
{
for(int x=1;x<=n/k;++x)//枚举这一种选几个
{
for(int s=n;s>=x*k;--s)//枚举现在大小
{
A(f[c][s][0],f[c-1][s-x*k][0]*(S[k]-c+1)%mod*I[c]%mod);
}
}
}
for(int c=1;c<=min(n/k,S[k]);++c) for(int s=1;s<=n;++s)
A(f[0][s][0],f[c][s][0]),f[c][s][0]=0;
}
if(d==K/2+1&&(K&1^1)){for(int s=0;s<=n;++s) G[0][s][0][0]=f[0][s][0];break;}
for(int k=d-1;k<n;++k)//枚举转移大小(限制深度为 d-1)
{
for(int y=1;y<=k;++y)//枚举关键叶子个数
{
for(int c=1;c<=min(n/k,F[d-1][k][y]);++c)//枚举同一大小同一关键叶子个数选几种
{
for(int x=1;x<=n/k;++x)//枚举这一种选几个
{
for(int s=n;s>=x*k;--s)//枚举现在大小
{
for(int z=s;z>=x*y;--z)//枚举现在关键叶子个数
{
A(f[c][s][z],f[c-1][s-x*k][z-x*y]*(F[d-1][k][y]-c+1)%mod*I[c]%mod);
}
}
}
}
for(int c=1;c<=min(n/k,F[d-1][k][y]);++c) for(int s=1;s<=n;++s) for(int z=1;z<=s;++z)
A(f[0][s][z],f[c][s][z]),f[c][s][z]=0;
}
}
for(int s=1;s<=n;++s) for(int z=1;z<=s;++z) F[d][s][z]=f[0][s][z];
for(int s=1;s<=n;++s) for(int z=1;z<=s;++z) A(S[s],F[d-1][s][z]);
}
int d=K/2+1;
if(K&1)
{
ll ans=0;
for(int k1=1;k1<=n;++k1)
{
for(int y1=1;y1<=k1;++y1)
{
if(p%y1) continue;
int k2=n-k1,y2=p/y1;
if(k2<k1||(k2==k1&&y2<y1)) continue;
ans+=F[d][k1][y1]*F[d][k2][y2]%mod;
}
}
cout<<ans%mod<<'\n';return 0;
}
for(int k=d-1;k<n;++k)//枚举转移大小(限制深度为 d-1)
{
for(int y=1;y<=k;++y)//枚举关键叶子个数
{
for(int c=1;c<=min(n/k,F[d-1][k][y]);++c)//枚举同一大小同一关键叶子个数选几种
{
for(int x=1;x<=n/k;++x)//枚举这一种选几个
{
for(int s=n;s>=x*k;--s)//枚举现在大小
{
for(int z=s-x*y;z>=0;--z)//枚举转移前关键叶子个数
{
for(int a=p-(z*x*y+(y*y*x*(x-1)/2));a>=0;--a)//枚举转移前直径个数
{
A(G[c][s][z+x*y][a+z*x*y+(y*y*x*(x-1)/2)],G[c-1][s-x*k][z][a]*(F[d-1][k][y]-c+1)%mod*I[c]%mod);
}
}
}
}
}
for(int c=1;c<=min(n/k,F[d-1][k][y]);++c) for(int s=1;s<=n;++s) for(int z=1;z<=s;++z) for(int a=0;a<=p;++a)
A(G[0][s][z][a],G[c][s][z][a]),G[c][s][z][a]=0;
}
}
ll ans=0;for(int i=1;i<=n;++i) ans+=G[0][n][i][p];
cout<<ans%mod<<'\n';return 0;
}
9.29
一天才做了俩题啊,仍然效率低下,放完假回来不能这样了。
晚上被问 牛BC G,看了半天不会,后面就摆到放学。
LuoguP5161 WD与数列
独立做出来串串题了,难得啊。
考虑将原数组差分,将问题转化成在一个长度为 \(n-1\) 的序列中,找出来两个长度相同的子区间 \([l1,r1],[l2,r2]\) 使得它们相等并且 \(r1<l2-1\)。
对这个数组建 SAM,然后相当于统计 \(\sum\limits_p\sum\limits_{i,j\in endpos(p),i<j} \max(0,\min(len_p,j-i-1)-len_{fa_p})\)。
那么如果只在两个不同的位置 \(i,j\) 第一次合并到同一个 \(endpos(p)\) 时统计答案,就只需要统计 \(\min(len_p,j-i-1)\)。
于是在对每个节点开一个线段树维护 endpos 集合,合并的时候把集合小的拿出来在集合大的里面查询。
查询会将 \([1,n-1]\) 分成 \([1,x-len_p-1],[x-len_p,x-1],[x+1,x+len_p],[x+len_p+1,n-1]\) 四段,线段树上只需要维护区间中数的出现次数与区间和即可。
时间复杂度 \(O(n\log^2 n)\)。
LOJ#2720 「NOI2018」你的名字
开了好几次看过好几次题解,等到自己有点忘了,再自己做,没有一回能有思路。
这次也是不出例外的看了题解啊,看了一篇题解一半,又去看了小 E 的,写了小 E 的做法,等有机会了再用另一种方法来考一次自己。
考虑计算 \(T\) 与 \(S_{[l,r]}\) 有多少本质不同公共子串,再用 \(T\) 的本质不同子串个数减去。
对所有字符串分别建出来 SAM。
对一个串 \(T\) 记录一个 \(f_i\) 表示 \(T\) 的 \(i\) 前缀在 \(S_{[l,r]}\) 中匹配得到的最长后缀。
那么在 \(T\) 的 SAM 上的一个节点 \(p\),任取(因为都是等价的)一个 \(x\in endpos(p)\),在这个点 \(p\) 的所有贡献就是 \(\max(0,\min(f_x,len_p)-len_{fa_p})\)。
那么现在问题变成了求 \(f_i\),当询问的是一整个 \(S\) 时就相当于直接匹配。
不然我们就先将询问按右端点升序排序,不断从 \(1\to n\) 加入下标,然后动态维护 \(S\) 的 parent 树上每个点 \(p\) 的 \(endpos(p)\) 中的最大值,记为 \(MAX_p(p)\)。相当于单点赋值,子树求最大值,线段树维护之。
这样我们仍将 \(T\) 放到 \(S\) 上匹配,不过一个节点能匹配的最大大小就是 \(MAX_p(p)-l+1\)。类似双指针的,每次新加入一个点正常匹配,之后当 \(MAX_p(p)-l+1<len_{fa_p}+1\) 时就要缩短子串大小,在前面删掉一些字符,使得当前匹配的子串完全包含在 \(S_{[l,r]}\) 中。也就是向 parent 树上的父亲跳,最后仍然要将当前匹配的长度与 \(MAX_p(p)-l+1\) 取 \(\min\),这样我们就求出了每个 \(f_i\)。
我觉得这个我把代码写的很好看所以我要挂出来。
点击查看代码
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<string.h>
#define ll long long
using namespace std;
const int MAXN=1e6+10,MAXQ=1e5+10;
int q,dfn[MAXN],tot,siz[MAXN],f[MAXN];ll ans[MAXQ];
string S,T[MAXQ];vector <int> v[MAXN];
struct node{int l,r,pos;}p[MAXQ];
inline bool cmp(node x,node y){return x.r<y.r;}
struct SAM
{
int lst,cnt;ll sum=0;
vector <int> len,fa,pos,t[26],k;
vector < vector <int> > v;
inline void upd(int c)
{
int cur=++cnt,p=lst;len[lst=cur]=len[p]+1;
for(;p!=-1&&!t[c][p];p=fa[p]) t[c][p]=cur;
if(p==-1) return ;int q=t[c][p];
if(len[q]==len[p]+1){fa[cur]=q;return ;}
int cl=++cnt;fa[cl]=fa[q],len[cl]=len[p]+1;
for(int i=0;i<26;++i) t[i][cl]=t[i][q];
for(;p!=-1&&t[c][p]==q;p=fa[p]) t[c][p]=cl;
fa[cur]=fa[q]=cl;return ;
}
inline void init(string &s)
{
int n=s.size();s=' '+s,pos.resize(n+1);
len.resize(n<<1),fa.resize(n<<1),k.resize(n<<1);
for(int i=0;i<26;++i) t[i].resize(n<<1);
v.resize(n<<1),fa[0]=-1;
for(int i=1;i<=n;++i)
upd(s[i]-'a'),k[pos[i]=lst]=i;
for(int i=1;i<=cnt;++i)
v[fa[i]].push_back(i),sum+=len[i]-len[fa[i]];
}
void dfs(int x)
{for(int y:v[x]) dfs(y),k[x]=k[y];}
}s,t[MAXQ];
namespace Segment_tree
{
int num[MAXN<<2];
void upd(int l,int r,int p,int x,int z)
{
num[p]=max(num[p],z);
if(l==r) return ;int mid=(l+r)>>1;
if(x<=mid) upd(l,mid,p<<1,x,z);
else upd(mid+1,r,p<<1|1,x,z);
}
int query(int l,int r,int p,int x,int y)
{
if(x<=l&&y>=r) return num[p];
int mid=(l+r)>>1,ans=0;
if(x<=mid) ans=max(ans,query(l,mid,p<<1,x,y));
if(y>mid) ans=max(ans,query(mid+1,r,p<<1|1,x,y));
return ans;
}
inline int Q(int x)
{return query(1,s.cnt,1,dfn[x],dfn[x]+siz[x]-1);}
};using namespace Segment_tree;
void dfs(int x)
{
if(x) dfn[x]=++tot,siz[x]=1;
for(int y:s.v[x]) dfs(y),siz[x]+=siz[y];
}
signed main()
{
freopen("name.in","r",stdin);
freopen("name.out","w",stdout);
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>S>>q,s.init(S),dfs(0);
for(int i=1;i<=q;++i)
{
cin>>T[i]>>p[i].l>>p[i].r,p[i].pos=i;
t[i].init(T[i]),t[i].dfs(0);
}
sort(p+1,p+1+q,cmp);
for(int i=1,j=1;i<=q;++i)
{
for(;j<=p[i].r;++j) upd(1,s.cnt,1,dfn[s.pos[j]],j);
int pos=0,len=0,I=p[i].pos;
for(int k=1;k<T[I].size();++k)
{
while(pos&&!s.t[T[I][k]-'a'][pos])
pos=s.fa[pos],len=s.len[pos];
if(s.t[T[I][k]-'a'][pos])
pos=s.t[T[I][k]-'a'][pos],++len;
while(pos&&Q(pos)-p[i].l+1<s.len[s.fa[pos]]+1)
pos=s.fa[pos],len=s.len[pos];
len=min(len,Q(pos)-p[i].l+1),f[k]=len;
}
for(int k=1;k<=t[I].cnt;++k)
ans[I]+=max(0,min(f[t[I].k[k]],t[I].len[k])-t[I].len[t[I].fa[k]]);
}
for(int i=1;i<=q;++i) cout<<t[i].sum-ans[i]<<'\n';return 0;
}
9.30
今天写得多,但是不折叠,因为算是这一周的总结吧。
\(100+0+20+20\),rk9,烂完了啊,T2 被骗了,T3 感觉打表也找不出来规律就放弃了,T4 又忘了想分治。
其实像分治,点分治,网络流这种我不是很能想到的东西,已经注意让自己在赛时想一想了,可是每次注意到的时候都是并不是正解的时候,而每次真的是这个算法的时候又是从始至终想不到,怎么回事呢?
感觉这几天总体来说,状态一般吧,模拟赛时好时坏,但是好的时候也打不出来去年偶尔那种妙手的感觉。
但是模拟赛之外的时间仍然浪费很多啊,可是明天放假了,今天就先这样吧,回来以后一定要抓紧时间啊,不能总浪费,时间真的不多了。
感觉时间浪费的还有一个原因就是总在犹豫要不要开一个题,开哪个题,有时候开了一个题看了一会就弃了,以后尽量将这种情况减少到极少吧。
总是感觉自己是低水平选手,意思是越难的比赛打的越烂(不是指分,指的是可能跟一些人相比,NOIP 差距不大,但是到了后面人家能想到的正解也好部分分也好我都想不到),感觉跟自己高级知识点学的不多不熟有关系。
于是最近在尝试补一补字符串,每天强迫给自己喂字符串题,感觉好像不像之前这么抵触了,想要等到之后补补数学相关,其实很希望自己能学明白生成函数。
不过最后又看了看自己这一周做的题,发现量真少啊,之后一定得不浪费时间了,天天改完题就想摆,痛斥这种行为。
我去我这一周怎么 D 了自己三次“效率低下”,你改悔罢!!1
CSP-7
A.median
枚举中位数的值 \(x\),统计出来每个序列中小于 \(x\),等于 \(x\),大于 \(x\) 的值的个数,枚举 \(5\) 个数中有几个小于 \(x\),等于 \(x\),大于 \(x\) 的数,直接计算贡献。
B.travel
赛时被题面吓了,实际上是自己懒得思考。
一个图是有趣的相当于有一个多元环或者可以到达的多个自环,直接搜。
C.game
结论是如果所有数出现次数都是偶数则先手必败。
原因是当满足上述条件时,无论如何操作都会使得操作后不满足这个条件。
任何一个不满足这个条件的情况都可以通过操作最大值来满足条件。
D.counter
发现 \(x\) 一次最多 \(\pm 9\),也就是你在两个位置中间任取一长度 \(\geq 9\) 的段,其中有至少一个点一定被经过。
于是可以找出来连续 \(9\) 个点预处理出来 \(x\) 到它们的距离和它们到 \(y\) 的距离就能合并答案。
于是猫树分治,每次取 \([mid-4,mid+4]\) \(9\) 个点,每次 BFS 时只走在区间 \([l-30,r+30]\) 中的点(因为并不会离开区间很远)。
CF1037H Security
自己一个小时过 *3200 ?这 3200 是假的吧。
考虑怎么求出一个串 \(S\) 的子串中字典序大于 \(T\) 的字典序最小串。
肯定是想要贪心的匹配尽可能长的 \(T\) 的前缀,如果匹配了 \(i\) 位,再在后面加一个字符 \(c\) 使得 \(c>T_{i+1}\) 即可。于是对 \(S\) 建出 SAM 把 \(T\) 放上去匹配,匹配第 \(i\) 个字符前先查看自动机上转移字符 \(>T_i\) 的出边,尝试更新答案。如果某个时刻匹配不了就跳出。注意也可以匹配完整个串再在后面再加一个字符。
那么原问题是选出 \(S_{[l,r]}\) 中的一个子串,也就是我们查看出边时还需要检查对应节点的 \(endpos\) 集合中是否有 \([l+i-1,r]\) 中的下标(\(i-1\) 是已经匹配了的长度)。可持久化线段树合并维护一下 \(endpos\) 集合即可。