海亮集训总结
2025/1/17 整体二分
一种类似 cdq 的,在答案值域上批量处理询问并将询问分成两份的算法。详情见我的洛谷博客。
2025/1/18 模拟赛
T5
我是奶龙。T5是非常简单的容斥,但是我补题都补了1.5h。
题意
给定一个矩阵,每个格子上都有黑色或透明的颜色,你知道了从侧面看过去每行和每列的颜色(你知道了这一行是否有黑色的格子)。请问有多少可能的涂色情况满足条件。
解答
首先可以排除透明的行和列,因为它们别无选择,一定不能涂色。(这一步我就没想到!)问题变成了求 \(n\times m\) 的矩阵,每行每列都有至少一个黑色块的情况数。考虑容斥,\(f_i\) 表示最多有 \(i\) 行有黑色的情况数,\(f_i=\binom{n}{i}(2^i-1)^m\)。我们答案是恰好有 \(n\) 行有黑色的情况,因此需要容斥。答案就是:\(\sum\limits_{i=0}^n (-1)^{n-i}f_i\)。请留意此处是 \((-1)^{n-i}\) 而非 \((-1)^i\),这一现象的关键在于我们要先加上有 \(n\) 行有黑色的情况,再减去不满足答案的。
点击查看代码
#include <iostream>
#include <algorithm>
typedef long long ll;
using namespace std;
const ll N=1e6+10;
const ll mod=998244353;
ll n,m,w,d;
ll jc[N],njc[N];
ll fpow(ll x,ll y)
{
ll res=1;
while(y)
{
if(y&1)
res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
ll comb(ll x,ll y)
{
return jc[x]*njc[x-y]%mod*njc[y]%mod;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(ll i=1;i<=n;i++)
{
char ch;
cin>>ch;
w+=(ch=='B');
}
for(ll i=1;i<=m;i++)
{
char ch;
cin>>ch;
d+=(ch=='B');
}
n=w;m=d;
jc[0]=1;
for(ll i=1;i<=n;i++)
jc[i]=jc[i-1]*i%mod;
njc[n]=fpow(jc[n],mod-2);
for(ll i=n-1;i>=0;i--)
njc[i]=njc[i+1]*(i+1)%mod;
ll ans=0;
for(ll i=0;i<=n;i++)
{
if(i&1)
ans=(ans+mod-comb(n,i)*fpow((fpow(2,n-i)-1+mod)%mod,m))%mod;
else
ans+=comb(n,i)*fpow((fpow(2,n-i)-1+mod)%mod,m)%mod;
ans%=mod;
}
ans=(ans+mod)%mod;
cout<<ans<<endl;
return 0;
}
复习容斥原理:
设 \(f_i\) 表示最多有 \(i\) 个元素满足条件的情况总数,\(g_i\) 表示恰好有 \(i\) 个元素满足条件的情况总数。那么我们有:
请留意 \((-1)^{n-i}\) 的幂次。我们要先加上包含出现 \(n\) 次的情况,再将多余的减去。
T6
\(dis_{i,k}+dis_{k,j}\ge dis_{i,j}\)(\(dis_{x,y}\) 表示 \(x,y\) 之间的最短路)当且仅当 \(k\) 在 \(i,j\) 最短路上可取等。
这一个trick可用于优化很多暴力。
点击查看代码
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll N=500+10;
ll n,m;
ll dep[N][N],fa[N][N],dis[N][N],ans[N][N];
bool vis[N],check[N];
vector<ll> a[N][N];
void dfs(ll k,ll x,ll fa_)
{
dep[k][x]=dep[k][fa_]+1;
fa[k][x]=fa_;
for(auto to:a[k][x])
{
if(to==fa_) continue;
dfs(k,to,x);
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(ll i=1;i<=m;i++)
{
for(ll j=1;j<n;j++)
{
ll u,v;
cin>>u>>v;
a[i][u].push_back(v);
a[i][v].push_back(u);
}
for(ll j=1;j<=n;j++)
{
dep[i][0]=-1;
dfs(i,j,0);
for(ll k=1;k<=n;k++)
{
dis[j][k]+=dep[i][k];
}
}
}
/*
for(ll i=1;i<=n;i++)
{
for(ll j=1;j<=n;j++)
cout<<dis[i][j]<<" ";
cout<<endl;
}
*/
for(ll i=1;i<=n;i++)
{
for(ll j=1;j<=n;j++)
{
ll cnt=0;
for(ll k=1;k<=n;k++)
if(dis[i][k]+dis[k][j]==dis[i][j])
cnt++;
cout<<cnt<<" ";
}
cout<<endl;
}
return 0;
}
T7
题意
Interval Revisited
给定一系列区间,每个区间有权值,要求选取一些区间使得他们覆盖了1-m的所有点。每个点的费用是覆盖它的区间的权值之和,求所有点的费用最小值。
题解
https://blog.csdn.net/yjt9299/article/details/81304142
注意到区间存在包含关系则一定不优,因此同一个点不能被覆盖大于两次。我们设 \(dp_{i,j}\) 表示考虑前 \(i\) 个区间,选择第 \(i\) 个区间,\(j\) 到 \(r_i\) 只被覆盖了 1 次(其他部分不考虑)的答案。设计两个状态维度是为了方便计算一段点被覆盖的费用。
转移分两种:
\(r_k+1==l_i\):所以 \([l_i,r_i]\) 全都没被覆盖,\(dp_{k,1~r_k}\) 直接跟 \(w_i\) 取 max
\(r_k\in [l_i,r_i)\):有一段区间被覆盖,\(dp_{k,1~l_i}\) 跟 \(w_i+w_k\) 取 max
使用前缀最小值做优化。
Q:为什么代码中要转移到
dd[i][r[k]+1]?
A:因为区间不包含一定不劣,而l[i]递增,所以无需转移到只有区间包含才能访问的地方。
Q:为什么取
dp[i][m](前缀最小值数组)为答案可行?
A:因为只有dp[i][r[i]]及以前的位置不为inf(见前缀最小值的实现)
点击查看代码
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll N=3000+10;
const ll inf=0x3f3f3f3f3f3f3f3f;
ll n,m;
struct node
{
ll l,r,w;
}a[N];
ll dp[N][N],f[N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
ll T;
cin>>T;
while(T--)
{
cin>>n>>m;
for(ll i=1;i<=n;i++)
for(ll j=0;j<=m;j++)
dp[i][j]=inf;
for(ll i=1;i<=n;i++)
{
cin>>a[i].l>>a[i].r>>a[i].w;
}
sort(a+1,a+n+1,[&](node x,node y){if(x.l==y.l)return x.r<y.r; return x.l<y.l;});
for(ll i=1;i<=n;i++)
{
if(a[i].l==1)
{
for(ll j=1;j<=a[i].r;j++)
dp[i][j]=min(dp[i][j-1],a[i].w);
}
}
for(ll i=1;i<=n;i++)
{
//cout<<a[i].l<<" "<<a[i].r<<" "<<a[i].w<<endl;
for(ll k=1;k<i;k++)
{
if(a[k].r+1==a[i].l)
dp[i][a[k].r+1]=min(dp[i][a[k].r+1],max(dp[k][a[k].r],a[i].w));
else if(a[k].r>=a[i].l && a[k].r<a[i].r)
dp[i][a[k].r+1]=min(dp[i][a[k].r+1],max(dp[k][a[i].l],a[i].w+a[k].w));
}
for(ll j=1;j<=a[i].r;j++)
dp[i][j]=min(dp[i][j-1],dp[i][j]);
}
ll ans=inf;
for(ll i=1;i<=n;i++)
ans=min(ans,dp[i][m]);
if(ans==inf)
cout<<-1<<"\n";
else
cout<<ans<<"\n";
}
return 0;
}
T8
没看懂题解,看看这个?
https://qoj.ac/blog/hydd/blogs/161
2025/1/19 数据结构:线段树及其变体的应用
线段树进阶
[JSOI2009] 等差数列
可以看看第二篇题解,但注意状态定义。
首先做一个差分,转换为差分数组 \(a_i\)。那么一次修改就是两次单点加和一次区间加。
对于修改,我们发现一个等差数列是一个 \(abbb...bbb\) 这样的数列,其中 a 可以是任何值,我们称之为等差数列的头。
我们定义区间的 \(lans\) 是“第一个等差数列没有头,其余的可以有头”的最少分出的等差数列。\(lans_{区间}\) 表示 原数组 中该区间对应的答案,因此初始值有 \(lans_{[x,x)}=lans_{(x,x]}=1\) 而不是 \(0\)。(为了方便,第二篇题解和我中还是定义区间为差分数组的下标,但开闭是在原数组意义下的)
上述定义仅仅是为了解释初始值的设定,对理解转移帮助不大,因此给我留下了未解之谜。
显然合并两个区间的情况可分为以下四种:
- 仅当 \(a_{mid}=a_{mid+1}\),右半区间一整段被合并到左半区间的末尾。(因为右半区间没有“头”)\(lans_{[l,r]}=lans_{[l,mid]}+lans_{[mid+1,r]}-1\)
- 右半区间的第一个数字被作为“头”并到右半区间第一个等差数列。(把右侧的第一个数拿出来作为右半第一个等差数列的头)(在合并的时候才允许有“头”,为了避免整个区间的答案有“头”)\(lans_{[l,r]}=lans_{[l,mid]}+lans_{(mid+1,r]}\)
- 左半区间的最后一个数字被作为“头”并到右半区间第一个等差数列。(原因同上)\(lans_{[l,r]}=lans_{[l,mid)}+lans_{[mid+1,r]}\)
最后我们还需要维护 \(lans_{(l,r)}\),以维护上述的三个信息。同时维护区间左端和右端的 \(a_l,a_r\)。
答案要从 \(lans_{(s,t]}\) 中取,因为 \(lans\) 维护的是第一个等差数列没有“头”的答案,而我们允许第一个等差数列有“头”,所以做一个转换。
未解之谜:为什么采用原数组上开闭区间的定义,而不是差分数组上开闭区间的定义?这么做有什么差别呢?为什么不能用同一套转移?
教训:线段树维护的东西最好写成结构体方便merge
线段树优化建图
区间连边
Legacy
P6348 [PA2011] Journeys
如果区间中每个点都被连了一条边,可以建两颗线段树,一颗当做出度,一颗当做入度,两棵树的节点一一对应,注意边的方向。详情见题解。
2025/1/20 模拟赛
T2
题面
\(3\times 10^5\) 个操作,每次操作可以是:
- 把一个没有父亲的点的父亲设置为另一个点
- 把一个点的所有祖先都给一个特定的东西
- 查询一个点上是否有某个特定的东西
解法
一种简单和复杂度低的写法是:将3操作离线到2操作上合并为询问,然后把最后的树建出来,那么只需查询发送物品的点是否既在接收点的子树里,又与接收点联通两个问题即可。
我写的有点复杂:时间倒流(实际上没必要)再用树状数组维护一个点到树根的链的权值和,如果边断了权值就表为1,这样就可以算出两点是否联通。(HTC说他写了树链剖分+并茶集)‘
我调了两个小时。这暴露出来在考试条件下,我的调试能力偏弱,甚至依赖运气(体现在两年提高组的成绩落差上)。我需要加强码力!
T3
我忘了原题(hzwer的种花),该罚!
首先这个数据范围必须一眼状态压缩。
(仅在 \(a[j]>1\) 时转移)
这是倒着递推。每种花种下了就必须一直维持,不如种下后一直施肥直到可以撑到那个时间,倒着递推是为了知道还要多久,因为正着递推你不知道。
关于初始状态:可以令 \(\forall 0\le j < n, dp[1<<j]\leftarrow1\);也可以令 \(dp_{0}\leftarrow 0\) 同时转移方程的代价与 \(1\) 取 \(\max\),还要特判 \(a[i]=1\)。这是因为转移式把种花和施肥视为等价,但是忽略了本质区别:必须先种下花才有花,因此办花展需要每种花至少被种下过。相对来说第一种更好,因为无需特判 \(0\)。
2025/1/21 平衡树:FHQ-treap
我学习了splay!见博客。
2025/1/22 模拟赛
T1
给定一个序列,求序列所有子段的平均值第 \(k\) 大的是多少。范围 \(1e5\)。
想的太慢了,前30min没有任何思路,后来才慢慢发现可以二分。二分之后就变成很板子的二维数点了。
T3
给你一个长 \(10^5\) 的序列。给你 \(10^5\) 个询问,每个询问问 \([l,r]\) 大于等于 \(x\) 的数字有多少个。你需要强制在线做 \(10^5\) 次修改,每次修改一个位置上的数字,输出初始和每次修改后所有询问的答案。
非常简单(虽然我写了双log而正解是单log)。
注意到只需计算每次修改会影响多少询问即可。
- 正解一: 将询问分为两个后缀询问,放到主席树上(下标是询问点,权值是询问权值),修改时查询主席树上下标在修改点左侧、权值在修改权值影响范围内的区间个数即可。
- 正解二: 受影响的询问个数 = \(权值在受影响区间内的询问个数 - 权值在受影响区间内但是没有包括修改点的询问个数\)。开两棵主席树维护。
- 我的解法: 将询问放到线段树上,用vector存其权值,并使用标记永久化技巧。修改时单点查询值域在受影响范围内的区间个数即可。
T4
给定 \(10^5\) 堆石子,从上到下权值是 \(a_i,b_i,a_i\),必须先取走上面的才能取下面的。求限制了可取走的石子最大值的情况下得到的最大权值。对 \([1,3n]\) 的所有限制求答案。
注意到每堆石子可分为两个物品,\([1,a_i],[2,a_i+b_i]\) 任意组合可得出所有取石子的方案。
2025/1/23 欧拉回路
详见洛谷。
2025/1/24 模拟赛
T2
给定一个长为 \(10^5\) 的大字符串和一个长为 \(5\) 的模式串。从一个长与大字符串相同的空串出发,每次选一长为 \(5\) 的子段替换为模式串(可以覆盖在原先就有的字符上),求是否能变为大字符串。
我的做法是类状态机。\(dp_{i,j}\) 表示第 \(i\) 位可否是模式串中第 \(j\) 位。显然第 \(i\) 位的情况只与第 \(i-1\) 位有关。
T3
定义一个序列的 价值 为该序列的 本质不同 非空子序列数量,其中子序列可以为整体。
现在给定一个长度为 \(n\) 的序列,保证每一个数字都在 \(n\) 以内。
求它的 所有 非空子序列的 价值 的和。
摘抄题解:
计算一个序列的本质不同子序列数量显然应该使用子序列自动机,即 \(f_i\) 表示最后一个数为 \(i\) 的本质不同子序列数量,在序列末尾加入 \(x\) 时的转移为 \(f_x=1+∑\limits_{i=1}^nf_i\)。
然后考虑原问题,转化为原序列的每个元素有 \(\frac{1}{2}\) 的概率加入计算贡献的序列末尾,上述转移发生与不发生的概率各为 \(\frac{1}{2}\),那么此时的转移即为 \(f_x=\frac{1}{2}(1+f_x+\sum\limits_{i=1}^nf_i)\)。
最后输出 \(2^n\sum\limits_{i=1}^nf_i\)

浙公网安备 33010602011771号