专项训练2

看到好多人也写总结了,总之还是挺有用的,至少确保每一道题都搞懂了,虽然不一定会看。

贪心专题

1. [NOIP2015 普及组] 推销员

link:https://www.luogu.com.cn/problem/P2672

思路跟正解大差不差,但想的有点复杂了。先把所有的按疲劳值排个序,(这样省却了找最大疲劳值的过程),然后只用考虑第x大的和后面距离+疲劳值最大值的比较即可(累了,不想写了)

2. Two Heaps

link:https://www.luogu.com.cn/problem/CF353B

思路一开始想到了一些,就是把相同的数平均分到两堆里,但如果有多个数出现次数不同,最好是将大的放在后面,这样子就可以保持交替均分,避免有这种情况:

12 12 12 13 14 14 14 1512 12 14 14 分到一个堆里,因为那个13夹在中间。
排序写得有点不一样:

点击查看代码
bool cmp(node a,node b)
{
if(cnt[a.val]==cnt[b.val])
return a.val<b.val;
return cnt[a.val]<cnt[b.val];
}

以上就可以做到既可以排序(找相同的数)又能让出现多的数放的最后了。

3. Antichain

link:https://www.luogu.com.cn/problem/CF353E

感觉紫评的有点过了,蓝题还差不多。
任意两个点互相达不到说明这两个点之间不存在一条链(链的两个端点就是这两个点),使得这条链是同向的。显然这个图里有很多条这样的链,肯定取每一条链除了端点外的任意一个节点(总个数就是链的条数),至于端点,其实和链里面的点是一样的,如果这个端点左右两边都是端点且左右都没有取,那么这个点就可以被取。最后两个个数之和就是答案。代码也挺好写的,不知道为啥题解里写的那么长。

4. [AGC057A] Antichain of Integer Strings

link:https://www.luogu.com.cn/problem/AT_agc057_a

此题让我对二分的单调性多了一层理解。一开始还读错题了(bushi),以为只是后缀就行,想+写了半个小时才发现不对。这个题目上的样例解释一定不要看,误导了我半天。首先最后这个集合一定是一段连续的值,这个证明想一想就出来了,然后会发现每一个数的第一个比它大的包含它的数为 f(x)=min(10x,x+10len(x)) ,那么 f(x) 一定要大于 r 才能选进来。发现 f(x) 其实是有单调性的,二分出最小的x即可。

5. [USACO10MAR] Barn Allocation G

link:https://www.luogu.com.cn/problem/P1937

很容易得想到线段树维护区间加和区间最值,但始终想不出贪心策略。看了题解后理解了,大概就是两个策略:

  1. 右端点相同时,按照左端点从大到小的顺序判断。这个比较好理解,因为较大一点的区间代价也就更多,这样子就会使得较小一点的区间难以取到,不优。
  2. 按照右端点从小到大的顺序判断。题解里说的有点点复杂,感性理解一下,我们希望的是让区间以从前往后的顺序排列(以从后往前的顺序也行,只不过所有策略都是相反的,道理一样),这样子能使得前后区间互相影响得最小。一开始我在想为什么不以左端点从小到大排列,原因是整体是一个从前往后的趋势,如果有左端点靠左、右端点很靠右的区间,就很容易影响到后面,所以是按照右端点来排序的。

6. [USACO09OCT] Allowance G

link:https://www.luogu.com.cn/problem/P2376

每一个面额都能整除所有比它大的面额。这句话一定要仔细想想。会发现大面值的都可以被小面值的凑出来,所以小面值产生的浪费一定比大面值的少(例如:6<=5+5和1+5,1+5更优)。所以在选取的时候,应先选大面值的,直到差的钱比当前面值小,再往小了选。这个就比较像八下物理第一章讲的天平一样,砝码是从大往小放。还有一个策略是当前选完后如果还是不够,再从小往大找到第一个 ≥ 差的钱的面值,使得浪费最小。

7. Competition

link:https://www.luogu.com.cn/problem/CF144E

首先需要明确的是每个运动员最终到达的辅助对角线上的点是一个区间(且是最短路),我读题的时候差点读错了。然后这个题我比较纠结的点就是两个运动员不能在同一时间在同一格子里,它让我联想到前几天考试改的T3,但我总不能状态压缩每一个运动员的位置吧。然后就卡住了。
但其实仔细想想(看题解ing)会发现这个问题是可以解决的。两个运动员在同一格子里要么是最终在同一格子里,要么是路径有交叉。后者的解决方案大致就是这个图:

然后最终问题就是每个运动员在区间内选一个点,使得不重复且被选的运动员最多。最优策略为按照左端点从小到大排序,左端点相同时按照右端点从小到大排序。这个策略其实和第5题很像。然后拿堆优化即可(时复O(nlogn)。

8. Jeff and Permutation

link:https://www.luogu.com.cn/problem/CF351E

真的没啥思路。题解很神奇,原来每个数取反与不取反的逆序对数都不受其他数的改变而影响。对于当前数i:

  • 取反。前面比他小的数就比他大了 → 前面比他小的数的个数。
  • 不取反。后面比他小的数还是比他小 → 后面比他小的数。
    这两个值取 min 总共再求和即可。

But, 其实我当时并没有看懂。后来经过探讨并多换了几篇题解才理解,为什么取反和不取反只用考虑一边?为什么不用考虑其他数的正负?其实它省略了一个排序的步骤,从最大的 a[i] 开始:

  • 取正:由于这个数是最大的,所以前面的所有数取正或取负都不会大于这个数,也就是说对于前面的数不会产生逆序对。同时,因为这个数是最大的,所以后面的数不管怎么取,都一定比这个数小,对于后面的数就一定会产生 n−i 对逆序对。
  • 取负:由于这个数是最大的,所以前面的所有数取正或取负都不会小于这个数,也就是说对于前面的数一定会产生 i−1 逆序对。同时,因为这个数是最大的,所以后面的数不管怎么取,都一定比这个数大,对于后面的数就一定不会产生逆序对。

最大的数考虑完了其实可以删除了,后面计算时完全可以无视比他大的值,每次计算时都可以把它当成最大的数来看。至于排序,因为不影响什么所以就省略了。

9. [HEOI2015] 兔子与樱花

link:https://www.luogu.com.cn/problem/P4107

真的有点不想写了啊啊
还是写吧
自底向上从小到大贪心删节点。证明详见题解。

第10道貌似是基环树不会就跳过去了。这个题单也算是完结了吧。

并查集专项

第一道题太简单就不放了。

1. Tokitsukaze and Two Colorful Tapes

link:https://www.luogu.com.cn/problem/CF1677C

事实证明我现在还不能切掉绿题,我觉得并查集比较不好想到的就是如何建边。此题就是将每一位上的颜色建边,最后必定会形成多个环,我们希望上下差值和最大,就是环上一大一小一大一小放置。会发现最终的答案就为 2*(所有环上较大数的和-所有环上较小数的和) ,因为这些值最终都在 [1,n] ,所以就让大的数为后 n/2 个数,小的数为前 n/2 个数。

2. AND-MEX Walk

link:https://www.luogu.com.cn/problem/CF1659E

神仙题。首先发现这个答案只有0,1,2三种情况。如果出现2,它的末尾为0,怎么&也无法出现1。所以1和2不能同时出现。

  • 0的情况:
    230,指的是二进制下30位。再加上题目里的与运算,很容易想到开30个并查集,第 i 个并查集将所有边权第 i 位为1的点连接。因为答案是0,说明至少有一位全部为1,即u与v在至少一个并查集是连通的。

  • 1的情况:
    稍微复杂一些。答案为1,说明出现了0,但最后一位为0。因为0的情况已经排除了至少有一位全部为1的情况,所以没有任何一位全部为1,所以必定会有0,至于最后一位为0,可以将边权最后一位为0的点与一个虚点0连接(也是开30个并查集),判断u点(除了第一位,否则有可能是1)是否与虚点0联通。如果联通,说明这一位经过了几个1后碰到一个0,即这个边权一定是大于1的,保证了1绝不会出现。

  • 2的情况:除了上面两种的都为2。

放一个封装版并查集:

点击查看代码
struct dsu{
int fa[maxn];
void init()
{
for(int i=0;i<maxn;i++) fa[i]=i;
}
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
fa[find(x)]=find(y);
}
bool judge(int x,int y)
{
return find(x)==find(y);
}
}a[35], b[35];

3. Anton and Tree

link:https://www.luogu.com.cn/problem/CF734E

一开始思路错了,想得太简单。如果把同一个颜色的联通块合并为一团,最优方案为从根到叶子一层一层变色,答案即为树的直径/2。大部分做法都是用并查集合并,其实只需要找到两边颜色不同的端点并把边权赋为1即可。

4. Sanae and Giant Robot

link:https://www.luogu.com.cn/problem/CF1687C

神仙题*2。考虑将题目上的条件转化,c[i]=a[i]b[i]+c[i1],c数组也就是差值的前缀和。当且仅当 c[l1]==c[r] 时,区间 [l,r] 可改变,[l,r]这个区间变为 c[l1] 。我们最终的目标是让所有c值都为0,所以当 c[l1]==c[r]==0 时,改变他才是最优的。相当于选择两端点为0且能操作的区间进行改变,如果最终能使得所有c值都为0即为 YES,反之为 NO.

然后考虑这个操作顺序以及如何操作。整体思路很简单,就是将为0的左端点取出来,判断右端点是否为0,然后中间所有数清零,然后不断循环以上操作。需要用到 vector、set、queue
这些STL来维护,就是看码力了。

点击查看代码
//q为值是0的下标,s是值不是0的下标,vec[i]是i的另一个区间端点
while(q.size())
{
int i=q.front();
q.pop();
for(int j:vec[i])
{
if(c[j]==0)
{
int l=min(i, j), r=max(i, j);
set<int>::iterator it;
it=s.lower_bound(l);
while(it!=s.end()&&*(it)<=r)
{
c[*it]=0;
q.push(*it);
set<int>::iterator it1=it;
it++;
s.erase(it1);
}
}
}
}

5. [HEOI2016/TJOI2016] 树

link:https://www.luogu.com.cn/problem/P4092

感觉是后面几道题唯一一道在正常范围之内的。并查集的定义就很显然了,find(i) 就是i的最近的标记的祖先。当u节点被标记时,fa[u]=u,否则,fa[u]=father[u],就是很正常的查找、路径压缩。然后倒序处理每个询问,先将最终状态跑出来,然后就是删除标记的过程。每当一个点删除标记,它的fa值就指向他的父亲。至于为什么不能正序处理,是因为增加标记和删除标记是不同的,删除时,曾经那些答案为当前删除节点的都会随着并查集往上找,而增加是不会改变当前节点儿子的答案的。最后记得正序输出。

感觉自己代码能力很弱肿么办??

6. Nene and the Passing Game

link:https://www.luogu.com.cn/problem/CF1956F

难难难
比较简单的想法就是判断两个人是否能传球并连边,答案为联通块的个数,但时间复杂度为O(n^2)的,考虑优化。
先将题目给的式子转化一下,不妨设 i<j ,则 li+lj<=ji<=ri+rj,移项并整理可得 i+li<=jlj and i+ri>=jrj。设 i 的左区间为 [iri,ili] ,右区间为 [i+li,i+ri] ,则i和j能传球当且仅当i的右区间和j的左区间香蕉(将满足这一条件的点成为合法的点)。

我们可以建立一些虚点来表示合法的点。将每一个队员可以到达的区间转换为 左、右两个端点都是合法的点 的区间(即题解里的pre和next数组),将每个队员和他所到达的两个区间的右端点连边,再将右端点和前面到左端点为止的点两两连边。最后再判断有几个联通块。

至于覆盖区间,查找左端点、右端点等操作,可以利用差分的思想,覆盖这一区间相当于给这个区间全部+1,相当于左端点+1,(右端点+1)-1,累积起来就是覆盖次数。

7. Clearing Up

link:https://www.luogu.com.cn/problem/CF141E

判断-1 → 加S(条件:无环)→ 判断-1 → 加m(条件:无环)此时形成一颗生成树,加入的m边即为已确定的边 → 清空 → 加m直至m边数到达(n-1)/2(条件:无环) → 判断-1 → 加s(条件:无环)

8. [HNOI2016] 最小公倍数

link:https://www.luogu.com.cn/problem/P3247

半上午+一下午+半晚上,人要麻了。
首先将题目转化一下:等于说是 max(路径上的a(下面表示为ai))=a&&max(路径上的b(下面表示为bi))=b ,显然将<=a且<=b的边所对应的点互相连边,u到v连通且最大的a、b对应相等则输出Yes
至少到上面还是我能想出来的。
考虑将边按照a从小到大排序,将询问按照b从小到大排序。然后将边分块来处理(本质就是优化时间复杂度),排序后就可以找到每一条边i下,满足ai <=a且i所在块为 最后一个<=a的位置 所在块(有点绕,其实就是找到最后一个<=a的位置)的所有询问(为什么要找这些询问呢?这样子i及其以前的点都可以满足a值的限制了),将i及其前面的边按照b从小到大排序(及不及其无所谓,只要和代码下面的起始坐标对应即可)。此时 i这个块 前面的位置的a值 都满足上面找到的询问,再判断b值是否满足,满足就把对应的两点及其a、b值连边。对于i 到 i所在块的末尾,再一一判断并连边即可。但为什么要将1到i的边按照b从大到小排序呢?是因为上面连边的操作复杂度可能飙到 O(mlog(m)q),用一个指针l从1指向i,询问和边都经过排序就不需要将l前面的点重复连边了,这也是可撤销并查集的用处所在,连边后判断连通性和最大的a、b是否满足要求,最后将i 到 i所在块的末尾的连边操作撤销,继续下一个询问。

下一个题虽然写了但真的不想写总结了(

折半搜索专题

1.「NOIP2009」靶形数独

link:https://www.luogu.com.cn/problem/P1074

这个题显然不能直接爆搜,会直接T飞,考虑用一些优化:

  • 从0最少的开始搜。填过数独的都知道,肯定从0最少的那一行开始填,这样子尝试次数能减少很多,同理复杂度也将降低。

  • 你别说,其实就没啥优化了,好好打爆搜,注意判断行、列、九宫格不重复一些细节,总和不要最后求。

Tips:注意一开始已填好的数一定要记得算上去!!锅了0.5~1h不知道错哪了,[捂脸哭]

2. [CQOI2013] 新数独

link:https://www.luogu.com.cn/problem/P4573

这两道蓝题都挺水的(代码可能有点难调

思路很简单,爆搜9*9的格子,1~9判断填进去行不行。

3. Anya and Cubes

link:https://www.luogu.com.cn/problem/CF525E

比较常规的折半搜索题,三个分支:不加,加原数,加阶乘。显然我只需要算出左半边搜索时每一种k的个数中每一种答案的个数(用map存储),在搜索右半边的时候就可以直接累加。注意阶乘提前与处理出来,超过S就不要加了。

4. Two Fairs

link:https://www.luogu.com.cn/problem/CF1276B

偷一张图:

显然最后答案为左右绿框框里的点的个数的乘积(两边不能互相到达对面的点),比较显然的思路就是从a和b分别开始dfs,比如从a开始跑,遇到b就停止,记录能到达的点的个数,用n-个数就可以算出右边绿框框(b那一边)的点的个数,同理得到a那边的,最后乘积即为答案。

Tips:
不会清空edge数组?包教会的!

for(int i=1;i<=edgenum;i++) edge[i].next=edge[i].to=0;
for(int i=1;i<=n;i++) head[i]=0;
edgenum=0;

卡了1h啊

5. Expression

link:https://www.luogu.com.cn/problem/CF58E

这个题没怎么想就开始写了,莽了一堆没用的东西(也不能说没用,但我没有判断是否最短,整体思路还是有很多问题的)然后疯狂调,喜提10pts(只有样例过了),然后看了一眼题解,恍然大悟,直接开始重构了。
首先比较好想到的是当c=0时,直接让c变成a+b+进位即可,这就可以成为一个dfs的一个分支,其次就是两种情况讨论:

  • (a+b+进位)%10=c%10
    那就直接让a、b、c除以10(下取整),相当于说是dfs到更高一位。
  • 不能满足上一个时
    这时候就直接考虑给a或者b或者c的末位添加一个数字,使得个位成立,然后进下一层dfs。三个 分支,我一开始的时候想的是贪心取最小的加,事实证明显然假掉。

梳理出上面思路,就能看出这个dfs是从低位到高位的(一般人做计算题肯定也是从低位到高位算的,这个题同理),dfs时多带几个参数,再加一个可行性剪枝就可以了。

这个dfs代码真的让我收获颇深啊
void dfs(int a,int b,int c,int sum,int ta,int tb,int tc,int num,int cnt)
{
if(num>=ans) return ;
if(a==0&&b==0&&c==0&&sum==0)
{
ans=num;
aa=ta, bb=tb, cc=tc;
return ;
}
if(c==0)
{
int t=getlen(a+b+sum);
dfs(0, 0, 0, 0, ta+p[cnt]*a, tb+p[cnt]*b, tc+p[cnt]*(a+b+sum), num+t, cnt+1);
return ;
}
if((a+b+sum)%10==c%10)
{
dfs(a/10, b/10, c/10, (a%10+b%10+sum)/10, ta+(a%10)*p[cnt], tb+(b%10)*p[cnt], tc+(c%10)*p[cnt], num, cnt+1);
return ;
}
dfs(a*10+(c%10-b%10-sum+10)%10, b, c, sum, ta, tb, tc, num+1, cnt);
dfs(a, b*10+(c%10-a%10-sum+10)%10, c, sum, ta, tb, tc, num+1, cnt);
dfs(a, b, c*10+(a%10+b%10+sum)%10, sum, ta, tb, tc, num+1, cnt);
}

6. Distinct Paths

link:https://www.luogu.com.cn/problem/CF293B

嗯,这个题单大部分题的思路都可以想出来,但每次代码实现得很复杂,很难调,还容易TLE,就比如这个题。
主体思路我也不想赘述了,很简单,忘了的话去看下题解,但这个电风扇有很多技巧一定要学会。

  1. 剪枝:
  • 可行性剪枝:基本都会,也是非常实用的剪枝。
  • 最优性剪枝:跟上面的差不多。
  • 排除等效冗余★:当多个分支对答案的贡献是一样的,只需要算一种,其余的累积即可。比如这个题中,当一种颜色前面都没有染过时,显然染任何一种前面没有染过的颜色都是等价的,所以只用算一种。
  • 奇偶剪枝:详见 https://blog.csdn.net/qq_38790716/article/details/88051412
  1. 状态压缩
    f[x][y]表示到目前这个格子用到了哪些颜色,这样子就可以直接统计前面用到的颜色了(f[x][y]=f[x1][y]|f[x][y1])。然后加上一堆优化,就可以过去了。
点击查看代码
int dfs(int x,int y)
{
if(y>m)
{
x++, y=1;
}
if(x>n)
{
return 1;
}
int s=f[x-1][y]|f[x][y-1];
if(k-__builtin_popcount(s)<n+m-x-y+1) return 0;
int cnt=-1, ans=0;
for(int i=1;i<=k;i++)
{
if((a[x][y]&&a[x][y]!=i)||(s&(1<<(i-1)))) continue;
f[x][y]=s|(1<<(i-1));
// cout<<x<<" "<<y<<" "<<f[x][y]<<endl;
num[i]++;
if(num[i]==1)
{
if(cnt==-1) cnt=dfs(x, y+1);
ans=(ans+cnt)%mod;
}
else ans=(ans+dfs(x, y+1))%mod;
ans%=mod;
num[i]--;
}
return ans;
}

7. Bear and Square Grid

link:https://www.luogu.com.cn/problem/CF679C

思路很快出来了,但这个题的实现并不简单。看了眼题解,

优化:移动框时删掉最左边的一列,加上最右边的一列就是要求的数量

然后恍然大悟,但最可惜的是这个代码并没有完全是自己写的,借鉴了题解的框架,因为害怕自己像第5题一样写了一大堆最后调好久无果又得重构。但其实这个题想到这个优化了就没有很难了,我觉得它很像莫队,移动时只需要修改一些点的贡献即可。

后面两个题就不放了,思路比较简单,代码也并不是很难,一定要自己写。

线段树专题

1. [SDOI2014] 旅行

link:https://www.luogu.com.cn/problem/P3313

一眼树剖了,但什么动态开点线段树、树剖全都忘得差不多了,因为曾经学的很不好(主要是没有固定模版)今天必须要好好修整一下这个线段树的博客!

此题思路较简单,对每一个宗教开一颗线段树,修改时将这个点从原树删除,在加到新树里面,查询时套上树剖即可。

Tips:RE有可能是函数没有返回值。

2. 康娜的线段树

link:https://www.luogu.com.cn/problem/P3924

老实说这个题我想的其实挺对的,嗯,但我忘了线段树叶子节点的深度不一定都是一样的,然后我就误以为每一次的贡献其实就是 x(2maxdep1)(rl+1)qwq/n ,然后呵呵呵,全WA。我一直以为我思路有问题。

但是题解写的感觉挺玄乎,给我推式子啥的,我一开始还没看懂。于是我认认真真的看了一遍(包括代码),发现我除了前缀和以外都想到了,就是因为自己老以为线段树区间长度是2的幂,导致自己又丧失了一次锻炼代码能力的机会。

3. [JSOI2009] 等差数列

link:https://www.luogu.com.cn/problem/P4243

以前没有做山海经的回旋镖又打回我身上。合并时需要维护一大堆东西,这个pushup要考虑的很全面啊。
现在浅浅的整理一下思路:

  • 差分。因为题目里写了一大堆关于等差数列方面的修改,而且最终也是求等差序列个数,等差序列最显然的特点是什么?差相等啊,所以由此联想到差分。不过我一开始只以为差分数组中连续相等的x个数可构成一个长为x+1的等差数列,但是一些零散的数(也就是两个数)也可以构成等差数列。具体地,连续x个零散值可以构成长为x/2的等差数列。(自己手推一下即可)
  • 然后考虑合并。合并左右两个区间时需要哪些值?
    • 端点值,即左区间和右区间的左右端点值。这样子在合并的时候我才知道这两个区间中间是否可以形成一个新的等差数列。(以下将左区间的右端点和右区间的左端点称为中间的两个端点)
    • 零散值,即左右区间 分别 靠近左端点的零散值数量 和 靠近右端点的零散值数量。因为零散值也可以构成等差数列。如果不计算他的话,可能会漏掉一些更优的情况。
    • 等差序列划分数,即左右区间最少分别划分了几个等差数列。这个毕竟也是要求的答案嘛,对一些判断什么的都挺重要。
    • 其他,比如什么区间长度之类的,平常线段树要维护的东西。
  • 如何合并?(巨多巨烦人巨难调啊,这个分类讨论一定要注意细节)
    • 基本操作,包括端点值、区间长、划分数(现将原有的划分数加上,后来加上合并产生的新数列)等等。
    • 左右都全部为零散值。如果中间两个端点相同,那么当前这个区间的划分数+1,左零散值和右零散值分别为左右区间的 左右零散值-1。反之,当前区间左零散值和右零散值分别为左右区间的 左右零散值。
    • 只有左区间全部为零散值。当前区间的右零散值同上↑,如果中间两个端点相同,那么左零散值则为左区间左零散值-1,划分数+(右区间左零散值-1)/2+1(端点相同新增),反之,当前区间的左零散值直接变成左区间长度+右区间左零散值。
    • 只有右区间全部为零散值。同上,不说了。
      注意以上每一种情况讨论完都直接return。此时可以保证左右区间都有划分数,也就是说都有等差序列,那么此时当前序列的左右零散值就可以确定,然后进一步讨论。
    • 左区间没有右零散值且右区间没有左零散值。那么如果中间两个端点相同,原先就多算了一个划分数,--。
    • 只有左区间没有右零散值。如果中间两个端点相同,显然划分数加上(右零散值-1)/2,否则加上右零散值/2。
    • 只有右区间没有左零散值。同上,不说了。
      然后就到了最常规能想到的情况,真有点不想说了,继续常规判断,记得要和(左区间右零散值+右区间左零散值)/2取min。
  • 然后正常差分修改即可。注意最后答案也要和上面一样,把答案和总区间长度/2取min。

Tips:RE可能没判s==t的情况。

4. [USACO04DEC] Dividing the Path G

link:https://www.luogu.com.cn/problem/P1545

嗯,虽然暴力dp可以跑过去,但我还是用单调队列优化dp写了一发。

dp[i] 表示[0, i]这一区间最少个数,也就是说我们枚举这个喷灌的右端点(右边最大的范围),然后从前面转移即可,注意 [s[i]+1,e[i]1] 这个区间时不合法的,也就是他们的dp值为inf,其实也没啥了。。。

然后写了个单调队列优化版的(d[i]是差分数组判是否合法的):

点击查看代码
for(int i=2*a;i<=l;i+=2)
{
while(!q.empty()&&i-q.front()>2*b)
{
q.pop_front();
}
while(!q.empty()&&dp[q.back()]>=dp[i-2*a])
{
q.pop_back();
}
q.push_back(i-2*a);
if(!d[i])
dp[i]=dp[q.front()]+1;
}

5. Chain Queries

link:https://www.luogu.com.cn/problem/CF1975E

比较有意思的做法:考虑每增加一个黑点就给这个点--,父节点++,那么最终形成黑链必然是以下两种情况:

  1. 一个节点为-1,一个节点为1,其余全部为0.
  2. 两个节点为-1,两个节点为1,1中的两个点有一个是黑色的,另一个是他的父亲。

那set维护一下1和-1的个数就好了。

6. Two Subarrays

link:https://www.luogu.com.cn/problem/CF2042F

这个题单刷完许久才做这个题,当时觉得自己写不出来这个题,感觉好多细节不会,代码能力不行。现在细想了一下,根据自己的想法写了写pushup,看了看其他同学的代码,发现框架好像没啥问题,然后就开始自信地写啦!写完除了忘建树后就一发过了。

嗯,其实当时主要纠结两点:一是我没有搞清楚,不知道维护的什么pre1啊、suf1啊是最大的区间(题解省略了)。这个套路化的定义仿佛只有我不懂,多想想才 get 到了。二是边界处理,我还是没有很清楚的弄懂要维护什么。为什么要维护向左/右开的区间?主要还是归根于分类讨论里会有横跨的区间出现。为什么要维护区间 sum 值?因为sum是区间的中间部分,也就是说代码里的pre、suf是区间的边界,这样才能构成一个整体。pushup就是要从区间的组成来合并的。

图论相关问题

1. 牛场围栏

link:https://www.luogu.com.cn/problem/P2662

额,拿dp做的,参考的是这篇题解。然后其实我也并没有把他说的证明看得很懂,于是我尝试去学同余最短路,但和前面看证明一样感觉不是特别明白。有时间了要回来看看:https://oi-wiki.org/graph/mod-shortest-path

2.「LNOI2014」LCA

link:https://www.luogu.com.cn/problem/P4211

其实一下午状态并不是很好(原因是机房xxs乱叫),然后这个题思考的比较少。对这个题,会发现这样一个东西(偷图)

也就是把 [l,r] 这个区间的点到根节点路径上的点都标记一遍,然后最终就求z到根节点的标记总和。会发现时间复杂度的瓶颈在于需要遍历一遍[l,r]和q次询问,看到区间加会想到什么?线段树当然是差分啊,把1r的全部++,1l的全部--就可以了。至于q次询问,可以将所有区间排序,用一个指针表示l,直接一直加下去就可以了,对于每一个答案记下左右端点并减加。

3. 跳跳棋

link:https://www.luogu.com.cn/problem/P1852

神仙题。会发现每次跳要么是两边的往里(整体范围缩小)、中间的往外(整体范围扩大),虽然最大的跳的范围我们肯定是不能找到的,但是可以找到最小范围的呀(也就是三个棋子两两距离相同时)!而且每一种情况都是由最小范围跳来的,是不是很像一个根节点扩展到叶节点的过程?那么我们可以轻松地找到根节点,而题目就是让我们判断两种情况是否都在同一棵树上。可以像求LCA一样让两个节点向上跳,判断能否重合。具体实现可用二分。

下一道题 Berland and the Shortest Paths 详见专项训练1。

4. Breaking Good

link:https://www.luogu.com.cn/problem/CF507E

这个题单里唯一自己切掉的题。显然希望最终最短路里的修好的路最多,在跑dijkstra时同时更新修好的录得个数并记录路径即可。

5. Turtle and Intersected Segments

link:https://www.luogu.com.cn/problem/CF1981E

也挺神的。肯定是将边按 a 值排序,我们希望每条边和他的前一条边以及后一条边连线再能使得最终答案最小。但是这样子不能保证区间一定是香蕉的。于是我们考虑将区间以l为关键字排序,那么每次遇到l,就把它加到set里去,并与按照a值排序的前驱后继连边(set可自动排序),如果遇到r,就删除这条边,最后再建好的图上跑最小生成树。其实这个题和第二题差分处理区间的方式有点像,就是排序并线性处理,技巧就是贡献什么的一遍处理下来,只需对l、r对应影响的值作处理。

6. MST Company

link:https://www.luogu.com.cn/problem/CF125E

神仙题*3。首先去掉点1跑最小生成森林(由一堆最小生成树构成,树之间互相独立),剩下的就是考虑让1对这些树连k条边。如果1所连的边的个数比k还小,那么一定是-1。剩下的情况就是1有很多的连边,要求任选k条使得总和最小。这列涉及到一个知识点:破圈法,简单来讲就是

如果看到图中有一个圈,就将这个圈的边去掉一条,直至图中再无一圈为止。

那么这个题我们就可以先让1对每一棵最小生成树连最短的边,然后考虑增加(k-最小生成树个数)条边。对于每一棵最小生成树,让1再对他连一条边(此时就形成了一个环),然后将这个环上最大的边删去,这样子1的度数增加1。重复执行(k-最小生成树个数)就可以了。

7. DFS Trees

link:https://www.luogu.com.cn/problem/CF1707C

仔细阅读题目里给的伪代码会发现他是到一个节点从最小的边遍历下去,这样子就会形成“不归路”:

比如图上的红边本应该是最小生成树,但他走的黄边导致没有走权值为4的边。

会发现2、4、5其实形成了一个环,那么2-4和2-5就是树边,4-5就是横叉边。如果横叉边两边的点的子树作为根显然是可以的,因为他们必定会走最小生成树的边(较小),所以我们只需要给这两边的子树中的每一个点+1,最后统计整棵树,如果点权为(m-(n-1))=(m-n+1),即为横插边的个数,那么它就是合法的。至于如何巧妙地加点权呢?可以利用树上差分:sum[v]+=sum[u](v是u的儿子),具体就不说了。

提高刷题-dp

效率过于低下所以决定先把这个题单写写。

1. Min-Fund Prison (Medium)

link:https://www.luogu.com.cn/problem/CF1970G2

可行性dp(感觉自己做得挺少,并不是非常会做)。会发现需要添加的边数就是联通块个数-1,所以我们只需要最小化 a2+b2 即可。显然在(a+b)总和不变时,a、b差越小,答案就越小(a2+b2=(a+b)22ab 和一定,差小积大)。那么剩下的问题就是求出两个联通块大小所对应的答案了。删边操作要么删新加的边,要么删割边。考虑先将整张图边双缩点,如果原图是一个边双,直接输出-1。边双缩点会形成一个森林。然后设 dp[i][j][0/1] 表示到第i棵树时,第一个联通块大小为j,前面有没有删过边 是否可行。转移时只需要分类讨论:

  1. 以前删的树边 / 当前删的树边(分别删每一个子树统计答案)
  2. (当前是)第一个联通块 / 第二个联通块

然后枚举大小算出最小答案即可(其实跟前面差越小、答案越小没啥关联)。

2. Construct Tree

link:https://www.luogu.com.cn/problem/CF1917F

比较颓废吧,这个题不想写总结了。

3. Light Bulbs (Hard Version)

link:https://www.luogu.com.cn/problem/CF1914G2

首先第一问是很好处理的,从左往右没有染色就立马染色,然后对于相同数特殊处理一下即可。第二问就不会了。ㄟ( ▔, ▔ )ㄏ

于是我们先来学习一个有趣的trick:异或哈希。异或有什么性质呢?

  1. A ⊕ 0 = A
  2. A ⊕ A = 0
    也就是说一个数异或同一个数偶数次后,还是原来的这个数。这一点就很符合这一道题中每个数会出现两次的特点啊。哈希具体就不说了。我们维护前缀异或哈希和(下称为hsh),如果当前的hsh值为0,就说明前面的每个数都已经出现过偶数次了。

这个知识点对于这道题有什么帮助呢?来手玩一下样例(先不管哈希值):

上面那一排数字是前缀异或和,下面是原数,红框里下标为绿的数字就是能使得答案最小的染色的数。会发现第一问答案就是两个红框,答案显然是以0为分界线的。第二问的答案肯定是每一个红框里合法的数的个数的乘积。现在我们就是要求每一个0及其前面的合法数字个数。考虑维护一个 lst[i] 表示最后一个 hsh 值为 i 的位置,因为如果两个 hsh 值相同,那这个区间肯定包含它里面的区间,里面就不需要染色了,其余的就是染色的一种选择了。从这个红框的第一个位置跳 lst+1,统计个数并相乘即可。

4. Twin Friends

link:https://www.luogu.com.cn/problem/CF1906H

其实这个排列完全是不用管了,最后求出来再乘上 fac[n] 就行了。所以我们先钦定A是有序的,然后考虑给B填数。设 dp[i][j] 表示到了第 i 个字母(就是26个字母,比如a是第一位)、用了 j 个下一个字母(对于B来说的)的方案数。就是把A的第 i 个字母全部搞到A的尾部,然后填B。转移方程:dp[i][j]+=k=0cntb[i]cnta[i]+jdp[i1][k]×Ccnta[i]j 。把状态搞清楚转移就不难了。然后这玩意儿复杂度肯定超了,考虑前缀和优化。

5. One-X

link:https://www.luogu.com.cn/problem/CF1905E

蛮有意思的一道题。( ̄▽ ̄)"
首先对于一个点,如果它能产生贡献,那么他所表示的区间mid两侧各至少有一个叶子节点被选,方案数即为 (2lenl1)(2lenr1) 。设 f(u,len) 表示以 u 为根,u表示区间长度为 len 的答案。

然后写一个记忆化搜索:

点击查看代码
map<int,pair<int,int> > mp;//pair里存的是k和b
pair<int,int> dfs(int id,int len)
{
if(mp.find(len)!=mp.end()) return mp[len];
if(len==1) return mp[len]=(make_pair(1, 0));
if(len==0) return mp[len]=(make_pair(0, 0));
int lenr=len/2, lenl=ceil(len*1.0/2);
pair<int,int> lid=dfs(id*2, lenl), rid=dfs(id*2+1, lenr);
int k=(2*(lid.first+rid.first)%mod+(qpow(2, lenl)-1)*(qpow(2, lenr)-1)%mod)%mod;
int b=(lid.second+rid.second+rid.first)%mod;
mp[len]=(make_pair(k%mod, b%mod));
return mp[len];
}

6. Jellyfish and EVA

link:https://www.luogu.com.cn/problem/CF1874C

dp[u] 表示从 u 走到 n 的最大概率,非常简单的,dp[u]=dp[v]p[uv]。然后考虑这个 p[uv] 该怎么求。设 g[i][j] 表示出度为 i 的第 j 优的下一个节点的概率。怎么转移呢?将出点优先度从大到小排,最优选法肯定是选第一个(题目里的v),但是再选一条不一定是第一个(题目里的w,下面的k),分为两种情况:

k在j前面时,显然1,k这两点消除,那么总点数为 i2 ,j就变成第 j2 优的了,选k的概率是(j-2)/2。

k在j后面时,显然1,k这两点消除,那么总点数为 i2 ,j就变成第 j1 优的了,选k的概率是(i-j)/2。

g[i][j]=g[i2][j2]×j22+g[i2][j1]×ij2

至于初始状态,g[i][1]=1i

提高组生成树和笛卡尔树专题

1. [COCI 2009/2010 #7] SVEMIR

link:https://www.luogu.com.cn/problem/P8074

基本上可以想出来?但还是看了眼题解的啊。。。很显然的最小生成树板子,但一开始的边肯定不能 O(n2) 全加进来,会TLE也会MLE,所以考虑删掉一些不必要的边。发现如果 a.x<b.x<c.x ,就只需要让a和b连,b和c连,所以分别给a、b、c从小到大排序,相邻的点连边即可,最后再跑一个 kruskal。

2. [SCOI2012] 滑雪

link:https://www.luogu.com.cn/problem/P2573

想出来2/3吧,但最重要的细节没想到。显然跑一个dfs就可以找出最多能到达的点了,然后将这些点的连边存图再跑kruskal就行了。但是注意dfs跑出的是一个有向图,而kruskal是一个针对无向图的最小生成树算法!所以最后排序的时候要以边的终点的高度为第一关键字从大到小排,以边权为第二关键字从小到大排,这样子就能确保每次加入的点的高度都比以前的小,到每个点时指向它的边一定都被考虑完了,使得每条边都能考虑到。最好先判断高度再加边,否则后面处理会很麻烦。

3. [USACO01OPEN] Earthquake

link:https://www.luogu.com.cn/problem/P4951

虽然用kruskal跑一遍就可以得到88pts(就一个点没过去),但还是老老实实学正解了。分数规划?看不懂:链接。姑且只看这道题吧。

4. [蓝桥杯 2023 省 A] 网络稳定性

link:https://www.luogu.com.cn/problem/P9235

和货车运输是一样的。先跑一遍最大生成树,然后倍增维护最小值,lca查询即可。

5. [THUPC2022 初赛] 最小公倍树

link:https://www.luogu.com.cn/problem/P8207

看了题解一眼顿时会了,原先我的思路是枚举l到r的倍数然后连边,然后l到2*l同l连边,再把所有没有连的点同l连边,但是显然他是不够优的。有共同质因子的数连边 总比 没有共同质因子的数连边 要好,所以应该枚举这个质因子(而不是单纯的倍数关系),kx,(k+1)x,(k+2)x...(k+p)x 这些数都和 kx 连边是最优的( kx 为第一个大于等于l的),记得随后把 kx 也两两相连。

6. [BJWC2010] 严格次小生成树

link:https://www.luogu.com.cn/problem/P4180

次小生成树有一个性质:只有一条边和最小生成树不同。于是这个题我们可以枚举要加入哪一条边(即非树边),然后把可以删的边中最大的边删去,但这条变不能和加入的边相等!所以除了维护路径上的最大值,还要维护次大值。和第4题一样的方法,倍增维护这两个值即可。

次大值是怎么维护的呢?
for(int i=1;i<=18;i++)
{
f[u][i]=f[f[u][i-1]][i-1];
p[u][i]=max(p[u][i-1],p[f[u][i-1]][i-1]);
g[u][i]=max(g[u][i-1],g[f[u][i-1]][i-1]);
if(p[u][i-1]>p[f[u][i-1]][i-1]) g[u][i]=max(g[u][i], p[f[u][i-1]][i-1]);
else if(p[u][i-1]<p[f[u][i-1]][i-1]) g[u][i]=max(g[u][i], p[u][i-1]);
}
怎么查询的呢
int qmax(int u,int v,int maxx)
{
int ans=-1e18;
for(int i=18;i>=0;i--)
{
if(dep[f[u][i]]>=dep[v])
{
if(maxx!=p[u][i]) ans=max(ans, p[u][i]);
else ans=max(ans, g[u][i]);
u=f[u][i];
}
}
return ans;
}

整体看起来也没有很难。

7. [APIO2013] 道路费用

link:https://www.luogu.com.cn/problem/P3639

手模样例时发现,新边的费用受制于周围几条边,但具体是怎么回事,并不是很清楚,果然紫题还是很难的啊,应该是这个题单的分水岭吧。
发现k最大只有20,这启示我们往 2k 去想,也就是说可以枚举选取哪些新边,然后统计。但这样子跑最小生成树显TLE,考虑优化。
先将k条新边全加进这个图里(边权为0)然后跑最小生成树,显然他一定会选所有新边,那么剩余的原边就是必定会选进最终的最小生成树里的边。跑的时候把原边用并查集存储起来,就会形成多个联通块,然后考虑将这些联通块缩点(缩点的本质就是优化,不再考虑这些已确定的边)。
接着再用原边在已缩点的图上(现在这个图上没有边)跑最小生成树,得到的边就是有可能成为最终最小生成树里的边。记为 t。
然后是这么一句话:

现在 在枚举边集的时候跑的最小生成树 只要在缩点后的图上把枚举的边集加入 再用刚才得到的边集跑最小生成树 就行了。

因为最小生成树有一个性质,对于一个最小生成树T,往里面加入一条边e,那么就会形成一个环,环上除了e的边一定都比e小。于是我们可以用这个性质来约新边的费用,具体地,先把枚举的边集加入,用t跑最小生成树,t里的每条非树边u-v,树上u到v的边都要比u-v这条边小,用lca暴力跳父亲即可。

8. Mathematics Curriculum

link:https://www.luogu.com.cn/problem/CF1580B

O(n5) 过100。

神奇的笛卡尔树题(笛卡尔树是什么注意,笛卡尔树权值也可以是满足大根堆性质的)。画几棵大根堆性质的笛卡尔树可以发现,对于一个节点x,他所在区间的最大值树就是他在笛卡尔树中的深度(根节点深度为1)。这个可以感性理解一下,只有比他大的数能当他的祖先。所以这个题就转化为求有多少棵有多少形态不同的笛卡尔树满足其层数为 m 的节点数等于 k。

发现是个dp。设 dp[i][j][k] 表示子树大小为 i ,有 j 个深度为 k 的节点的数量(这个深度是相对于这棵大小为i的笛卡尔树的)。对于 ij 这两维,直接枚举即可,转移方程也是比较好想的:

dp[i][j][k]=a=0i1l=0jdp[a][l][k1]dp[i1a][jl][k1]Ci1a

初始状态比较多:

  • dp值为1:dp[1][1][1],dp[1][0][0],dp[0][0][i](0im),dp[1][0][i](2im).
  • g[i] 表示大小为 i 的笛卡尔树的数量。dp[i][1][1](2in),dp[i][0][k](2in,i<km) 值为 g[i]

零散的剪枝:

  • m==n&&K>1 时,直接输出0.(不加会T)
  • if(!dp[a][l][k-1]||!dp[i-1-a][j-l][k-1]||!c[i-1][a]) continue;,因为乘法很慢,有0就不要转移了。

9.「OICon-02」maxiMINImax

link:https://www.luogu.com.cn/problem/P10173

这个题跟笛卡尔树有啥关系啊,这个单调栈+树状数组优化的做法要简单很多。感觉这种题见到很多遍了,但好像也没有固定的套路?还是dp常见一些?算了不管了。
首先有两个很重要的点:

  1. 这是个排列,所以不存在用区间内两个等值极值算了重复的该区间。
  2. 相交的区间是没有贡献的。

仔细想想就能明白。所以发现对答案有贡献的区间必须满足 min[l2,r2]>=max[l1,r1]max[l3,r3].那么从小到大枚举每个数作为第二个区间的最小值,设 mn[i] 第i个数作为区间最小的区间个数,mx[i] 第i个数作为区间最大的区间个数。答案即为:

j=1pos[i]1k=pos[i]+1nmn[i]×mx[j]×mx[k]×(ia[j])(ia[k])

化简一下式子发现只需要用树状数组维护 mx[j]mx[j]a[j] 即可。

根号算法

暴力的优化,感觉有点像折半搜索一样分成两部分来降低复杂度。

1. Tree Master

link:https://www.luogu.com.cn/problem/CF1806E

根号分治,如果一层点数少于 n 就记忆化搜索,否则直接搜索。对于记忆化搜索部分,用数组存储:f[u][p[v]]p[v] 表示v在 dep[v] 这一层是第几个,这样子最差时间复杂度只会到 O(nn) (即全部填满),对于纯搜索部分,时间复杂度为 O(qn)

2. [COTS 2024] 双双决斗 Dvoboj

link:https://www.luogu.com.cn/problem/P10680

首先可以用倍增跑个暴力,但如果是修改,就需要修改 [p(1<<k)+1,p] 这个区间,加上外面的q复杂度可到达 O(nq),考虑根号分治,只用维护 2k<n 这部分的k(k最大到8),修改复杂度降到 O(qn) 。查询的话,dfs即可,如果此时 k <=8 直接输出,这样递归层数只有 n 层。

3.「SMOI-R1」Apple

link:https://www.luogu.com.cn/problem/P10408

神仙题啊,非常考验对题解的理解能力和代码能力。我们可以暴力修改、O(1) 查询或者 O(1) 修改,暴力查询。根号分治就是让这个时间复杂度均衡下来,是两个暴力的结合。

首先来学习一下 SOS dp(子集和dp)。显然,如果这个题没有修改,提前预处理出dp数组,O(1) 查询,总时间复杂度为 O(2nn) 。盲猜一波时间复杂度为 O(q2n/2) ,暴力预处理出子集和dp里的 S(sta,n/2) ,然后修改时也只修改后 n/2 位的,查询就查询第 (n/2)+1 到更高位与 sta 不同的 sta 的子集 S(x,n/2) 之和。

代码实现又简洁又高效:

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n, q, v[(1<<20)+5], dp[(1<<20)+5];
inline ll read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
x=x*10+ch-'0',ch=getchar();
return x*f;
}
int main()
{
n=read(), q=read();
for(ll i=0;i<(1<<n);i++)
{
v[i]=read();
}
for(ll i=0;i<(1<<n);i++)
{
dp[i]=v[i];
}
for(ll i=0;i<(n>>1);i++)
{
for(ll s=(1<<n)-1;s>=0;s--)
{
if(s&(1<<i)) dp[s]+=dp[s^(1<<i)];
}
}
ll tot=(1<<(n>>1))-1;
while(q--)
{
ll opt=read(), a, s;
if(opt==1)
{
s=read();
ll sum=0;
for(ll i=0;i<=(1<<(n-(n>>1)))-1;i++)
{
if(((s>>(n>>1))|i)==(s>>(n>>1))) sum+=dp[i<<(n>>1)|(s&tot)];
}
cout<<sum<<"\n";
}
else
{
s=read(), a=read();
for(ll i=0;i<=tot;i++)
{
if((s&tot&i)!=(s&tot)) continue;//不是后n/2位的子集
dp[(s^(s&tot))|i]+=a-v[s];
}
v[s]=a;
}
}
return 0;
}

4. Till I Collapse

link:https://www.luogu.com.cn/problem/CF786C

5. Mr. Kitayuta's Colorful Graph

link:https://www.luogu.com.cn/problem/CF506D(一定不要看成其他题了)

其实这个题时间复杂度我并不是很明确啊,可能是 unordered_map 查找速度很快吧。。。

首先有个暴力的思路就是对每一种颜色建图,判断连通性。考虑优化。每次判断只需要判断 u、v中度数小的点的连边,如果u、v已查找过就直接返回答案。然后加上这两个优化其实就可以过了。用 unordered_map 开 fa 数组,维护每一个点对应颜色的并查集父亲。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, m, q, num[maxn], vis[maxn];
int head[maxn], edgenum;
unordered_map<int,int> fa[maxn], ans[maxn];
int find(int x,int col)
{
return x==fa[x][col]?x:fa[x][col]=find(fa[x][col], col);
}
void merge(int x,int y,int z)
{
if(!fa[x].count(z)) fa[x][z]=x;
if(!fa[y].count(z)) fa[y][z]=y;
if(find(x, z)==find(y, z)) return;
fa[find(x, z)][z]=find(y, z);
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x, y, z;
cin>>x>>y>>z;
merge(x, y, z);
}
cin>>q;
while(q--)
{
int u, v;
cin>>u>>v;
if(fa[u].size()>fa[v].size()) swap(u, v);
if(ans[u].count(v))
{
cout<<ans[u][v]<<"\n";
continue;
}
int cnt=0;
for(auto x:fa[u])
{
if(fa[v].count(x.first)&&find(u, x.first)==find(v, x.first)) cnt++;
}
ans[u][v]=cnt;
cout<<cnt<<"\n";
}
return 0;
}

6. ycz的妹子

link:https://www.luogu.com.cn/problem/P4879

纯纯线段树。删除操作只需要维护一个cnt记录妹子的个数,和权值线段树中查找第k小数很像。

7. Robin Hood Archery

link:https://www.luogu.com.cn/problem/CF2014H

发现只有在平局的时候才会赢,也就是当每个数都在这个区间出现偶数次时才会赢。妥妥的异或哈希啊。

附:哈希随机生成:mt19937_64 rnd(random_device{}());

8. [THUPC 2017] 钦妹的玩具商店

link:https://www.luogu.com.cn/problem/P7432

热泪盈眶了啊,改了两个细节错误交上去直接A了,这个代码是为数不多自己想了很多的(虽然1/2代码看的题解)。

强制在线,考虑分块。很显然的想法就是对于每一对 [l,r] 整块提前预处理出来然后加上零散的值,套一个多重背包就行,但是单次查询复杂度为 O(m2+msqrtn) ,还要优化。
f[l][r][x]lr 表示 lr 所在块)表示花费 x 的代价时,[1,l][r,n] (就是去除了区间 [l,r] )的最大愉悦度,其中区间 [1,l] 可以在遍历到每一个块时用一个指针从1扫过来(但代码实现略有区别),后者则用一个指针从最后扫过来,计算时直接合并两部分,然后和刚刚的想法没啥区别。多重背包可采用二进制或单调队列优化。

点击查看代码
//分块+多重背包,感觉自己的想法已经接近于题解了
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e3+5, mod=1e8+7;
int T, n, m, q, c[maxn], v[maxn], t[maxn], st[50], ed[50], a[maxn];
int f[50][50][maxn], g[maxn], pos[maxn], ans=0, len, tmp, cnt, w[maxn], ans1;
void build()
{
len=tmp=0;
len=sqrt(n);
tmp=n/len;
if(n%len) tmp++;
for(int i=1;i<=tmp;i++)
{
st[i]=(i-1)*len+1;
ed[i]=i*len;
}
ed[tmp]=n;
for(int i=1;i<=n;i++)
{
pos[i]=(i-1)/len+1;
}
}
void get(int l,int r)
{
cnt=0;
memset(a, 0, sizeof(a));
memset(w, 0, sizeof(w));
for(int i=l;i<=r;i++)
{
int k=1, s=t[i];
while(k<=s)
{
cnt++;
a[cnt]=k*v[i];
w[cnt]=k*c[i];
s-=k;
k*=2;
}
if(s>0)
{
cnt++;
a[cnt]=s*v[i];
w[cnt]=s*c[i];
}
}
}
void init(int m)
{
memset(f, 0, sizeof(f));
memset(g, 0, sizeof(g));
for(int i=0;i<=tmp;i++)
{
get(st[i], ed[i]);
for(int j=1;j<=cnt;j++)
{
for(int k=m;k>=w[j];k--)
{
g[k]=max(g[k], a[j]+g[k-w[j]]);
}
}
for(int j=0;j<=m;j++) f[i][tmp+1][j]=g[j];
for(int j=tmp;j>=i;j--)
{
for(int k=0;k<=m;k++) f[i][j][k]=f[i][j+1][k];
get(st[j], ed[j]);
for(int k=1;k<=cnt;k++)
{
for(int p=m;p>=w[k];p--)
{
f[i][j][p]=max(f[i][j][p], a[k]+f[i][j][p-w[k]]);
}
}
}
}
}
signed main()
{
cin>>T;
while(T--)
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++) cin>>c[i];//重量
for(int i=1;i<=n;i++) cin>>v[i];//价值
for(int i=1;i<=n;i++) cin>>t[i];//个数
build();
init(m);
ans=ans1=0;
for(int i=1;i<=q;i++)
{
int x, y;
cin>>x>>y;
int l=min((x+ans-1)%n+1, (y+ans-1)%n+1);
int r=max((x+ans-1)%n+1, (y+ans-1)%n+1);
if(l>r) swap(l, r);
memset(g, 0, sizeof(g));
for(int j=0;j<=m;j++) g[j]=f[pos[l]-1][pos[r]+1][j];
get(st[pos[l]], l-1);
for(int j=1;j<=cnt;j++)
{
for(int k=m;k>=w[j];k--)
{
g[k]=max(g[k], a[j]+g[k-w[j]]);
}
}
get(r+1, ed[pos[r]]);
for(int j=1;j<=cnt;j++)
{
for(int k=m;k>=w[j];k--)
{
g[k]=max(g[k], a[j]+g[k-w[j]]);
}
}
ans=ans1=0;
for(int i=1;i<=m;i++) ans=(ans+g[i])%mod, ans1^=g[i];
cout<<ans<<" "<<ans1<<endl;
}
memset(f, 0, sizeof(f));
}
return 0;
}

其实我觉得代码最巧妙的部分在从前往后扫过来这块,他没有用指针,而是用数组存储,其实本质也就只是往g数组里添加当前块的贡献。下面列了几个代码语句,是我写的时候容易忘的(也是代码实现很重要的部分),复习时注意看一下

点击查看

for(int j=0;j<=m;j++) f[i][tmp+1][j]=g[j];
for(int k=0;k<=m;k++) f[i][j][k]=f[i][j+1][k];
for(int j=0;j<=m;j++) g[j]=f[pos[l]-1][pos[r]+1][j];

9. XOR and Favorite Number

link:https://www.luogu.com.cn/problem/CF617E

其实是很裸的莫队,求出前缀异或值 pre,对于一个区间 [l,r] ,他的异或值为 pre[r]pre[l1] (跟前缀和挺像的),所以我们要求所有 pre[r]xorpre[l1]=klr 的数量,因为 ab=cac=b ,所以每加入一个点,答案就加上(他异或k)这一值的数量即可。

树上问题

1. Minimum spanning tree for each edge

link:https://www.luogu.com.cn/problem/CF609E

很显然的想到要维护环上最大的边权,答案就是最小生成树边权和+当前边-当前边形成的环上的最大边(除去当前边)。但我以为并查集的联通块就是不同的环,然后还挺搞笑的😂。发现是个很常见的套路,我用的倍增维护每个点到lca的最大值,最后两个点取max即可。

2. Information Reform

link:https://www.luogu.com.cn/problem/CF70E

树形dp,设 dp[u][i] 表示点u从i获取信息,u子树的答案。显然 dp[u][i]=dis[u][i]+k ,记使得 dp[u][i] 最小的i为 p[u] ,然后从 v 转移,枚举信息中心,dp[u][i]=min(dp[v][p[v]],dp[v][i]k) ,再更新 p[u] 即可。答案再跑一个电风扇,判断 dp[v][ans[u]k]dp[v][p[v]] 的大小并更新就行了。
因为这个题数据范围比较小,这个想法也是比较清奇且暴力的,其实整体来看dp转移还是分成两部分,子树内(dp[v][p[v]]的和子树外的(dp[v][i]k),但我一开始状态就没想到啊。。。

3.「JZOI-1」旅行

link:https://www.luogu.com.cn/problem/P7359

考虑倍增维护这个时间,难点在于怎么样合并两条链。像线段树写pushup一样,分类讨论:

然后确实就是,倍增乱搞了。原先我自信满满写了一大坨(不要介意这个量词啊),然后终于调过样例了,交上去真的是1分不得,打对拍,最后改到这个代码我真的是接受不了了,于是去看了看wsq大佬的写法,发现自己有两个问题:

  1. 边界处理。就是链长度为1时的合并,其实非常简单啊,只需把a值赋为路上时间,d值赋为水上时间,然后c和d都设为inf就可以了,我自己特判了一堆,导致写的真的很麻烦。
  2. 最后两个链合并的时候,一定要把v所在链的b和c值交换,因为从v链所看的上端和下端与u链看到的上端和下端是相反的!

4.「DBOI」Round 1 人生如树

link:https://www.luogu.com.cn/problem/P9399

路径显然是不好维护的,考虑将它转变为一种值,即哈希。但现在还是不好判断相似度,考虑二分答案,每次判断两个hash值是否相等。因为a序列是由两部分组成的,所以维护 hsh[i][j] 表示从 i 开始到 2j 的祖先的链的哈希值,维护 rs[i] 表示从根到i点的哈希值,然后倍增乱搞,一些路径的哈希值可以 O(1) 求出,这里放一下gethash函数吧:

点击查看代码
ull gethsh(int x,int y,int lca,int len)
{
ull res=0, d=dep[x]-dep[lca]+1;
if(len<=d)
{
int depth=dep[x]-len+1, xx=x;
for(int i=18;i>=0;i--)
{
if(dep[f[xx][i]]>=depth)
{
xx=f[xx][i];
}
}
return rs[x]-rs[f[xx][0]]*p[dep[x]-dep[f[xx][0]]];
}
len-=d;
res=rs[x]-rs[f[lca][0]]*p[dep[x]-dep[f[lca][0]]];
int depth=dep[lca]+len, yy=y;
for(int i=18;i>=0;i--)
{
if(dep[f[yy][i]]>=depth)
{
yy=f[yy][i];
}
}
ull ans=0;
for(int i=18;i>=0;i--)
{
if(dep[f[yy][i]]>=dep[lca])
{
ans=ans*p[1<<i]+hs[yy][i];
yy=f[yy][i];
}
}
res+=p[d]*ans;
return res;
}

后面可能没有时间来写了,但今天(2025/2/10)真的效率很低,只做了2题,原因是上午被卡常,下午调不出来自己写的代码,于是在这里放一下还没跳完的代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int T, n, siz[maxn], fl[maxn], st[maxn], ed[maxn], t[maxn], son[maxn][2];
vector<pair<int,int> > del, plus_;
struct edge{
int next,to,w;
}edge[maxn<<1];
int head[maxn], edgenum;
void add(int from,int to)
{
edge[++edgenum].next=head[from];
edge[edgenum].to=to;
head[from]=edgenum;
}
bool cmp(int a,int b)
{
return fl[a]<fl[b];
}
void dfs(int u,int fa)
{
fl[u]=1, st[u]=ed[u]=u;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fa) continue;
dfs(v, u);
}
int num=0;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fa) continue;
t[++num]=v;
}
sort(t+1, t+num+1, cmp);
// cout<<"----------"<<endl;
// cout<<u<<endl;
int cnt=0, lst=u, flag=0, s, tot, p, flag1=0;
for(int i=1;i<=num;i++)
{
int v=t[i];
if(flag==0) s=ed[v], flag=1;
// cout<<"v:"<<v<<" "<<fl[v]<<" "<<st[v]<<" "<<ed[v]<<endl;
if(fl[v]==1)
{
cnt++;
if(cnt>=3)
{// cout<<u<<" "<<v<<endl;
del.push_back(make_pair(u, v));
plus_.push_back(make_pair(lst, st[v]));
}
if(cnt==1) p=v;
if(cnt!=1) lst=ed[v];
}
else
{
if(cnt==1&&flag1==0) lst=p, flag1=1;
// cout<<lst<<" "<<ed[v]<<endl;
del.push_back(make_pair(u, v));
plus_.push_back(make_pair(lst, st[v]));
lst=ed[v];
// cout<<lst<<endl;
}
}
// cout<<endl;
if(cnt<=1)
{
fl[u]=1;
if(lst==u&&cnt==1) ed[u]=ed[p];
else ed[u]=lst;
st[u]=u;
}
else
{
fl[u]=2, st[u]=s, ed[u]=lst;
}
// cout<<u<<" "<<fl[u]<<" "<<st[u]<<" "<<ed[u]<<endl;
}
int main()
{
// freopen("1.txt","w",stdout);
cin>>T;
while(T--)
{
cin>>n;
for(int i=1;i<n;i++)
{
int u, v;
cin>>u>>v;
add(u, v), add(v, u);
}
dfs(1, 0);
cout<<del.size()<<endl;
for(int i=0;i<del.size();i++)
{
cout<<del[i].first<<" "<<del[i].second<<" "<<plus_[i].first<<" "<<plus_[i].second<<endl;
}
del.clear(), plus_.clear();
for(int i=1;i<=n;i++) fl[i]=st[i]=ed[i]=0;
for(int i=1;i<=edgenum;i++) edge[i].next=edge[i].to=0;
for(int i=1;i<=n;i++) head[i]=son[i][0]=son[i][1]=0;
edgenum=0;
}
return 0;
}
/*
6
17 10 13 5
19 2 4 2
15 19 21 3
15 12 21 9
18 6 11 6
14 18 16 7
2 19 16 2
6 18 2 4
10 17 3 7
12 15 5 6
14 18 6 11
15 19 9 21
38
7 36
13 5
16 27
4 22
6 38
37 5
20 1
30 1
24 19
11 5
33 1
27 31
10 30
23 34
1 6
19 1
1 21
1 3
20 25
30 22
16 14
32 30
33 28
19 2
9 14
36 23
1 14
12 8
23 35
16 8
1 5
20 17
26 30
29 19
30 23
15 6
18 30*/

明天好好打模拟赛哦。


是什么战胜了贝多芬
Angel take your wings and fly watching over me
See me through my night time and be my leading light
Angel you have found the way never fear to tread(践踏 v.)
You'll be a friend to me angel spread your wings and fly
Voces angelorum gloria dona eis pacem//我也不知道是什么语言,就是赞美
For you're always near to me in my joy and sorrow
For you ever care for me lifting my spirits to the sky
Where a million angels sing in amazing harmony(和谐 n.)
And the words of love they bring to the never ending story
A million voices sing to the wonder of the light
So I hide beneath(在...下方 prep.) your wing
You are my guardian(守护者 n.) angel of mine
Cantate caeli chorus angelorum
Venite adoramus in aeternum
Psallite saecula et saeculorum
Laudate Deo in gloria
Can you be my angel now watching over me
Comfort and inspire me to see our journey through
Can I be your friend indeed from all cares set free
The clouds would pass away then I'd be an angel too
Voces angelorum gloria dona eis pacem
For you're always near to me in my joy and sorrow ❤
posted @   zhouyiran2011  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示