2025 平邑一中集训 笔记
Day1
摸底考试,rk10,难度黄绿绿蓝蓝
T1-灭蚊(kill)
现在有 \(n\) 只蚊子排成一排,每只蚊子都有一个威胁程度,第 \(i\) 只蚊子的威胁程度为 \(a_i\) (\(a_i \geq 0\))
你每枪可以击杀一名蚊子,按照给定顺序 \(p\) 击杀, \(n\) 枪以后一只不留。
但是蚊子也在寻找你,每当你击杀一只蚊子,你需要知道未被杀死的蚊子组成的队伍中最大的连续威胁程度和是多少,如果你杀光了所有蚊子,最大和就是 \(0\)。
原题,对应难度为黄
- 将所有操作倒序处理,由此题意转化为了添加一个数,求当前序列的最大子段和
- 具体可以用并查集维护当前序列每一个数所在区间的左端点和右端点,那么通过前缀和即可求该区间的和
- 每一次操作将新产生的区间的和扔进一个大根堆中,每一次操作的答案即为大根堆的顶端
- 时间复杂度为 \(O(n \log n)\),可以通过.
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,a[N],sx[N],qzh[N],anss[N];
priority_queue<int,vector<int>,greater<int> > ans;//大根堆,计算每一次操作的答案
int le[N],ri[N];//代表节点i所在区间的左端点 节点i所在区间的右端点
int ri_to(int x){
if(ri[x]==x) return x;
else return ri[x]=ri_to(ri[x]);
}
int le_to(int x){
if(le[x]==x) return x;
else return le[x]=le_to(le[x]);
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) qzh[i]=qzh[i-1]+a[i];//前缀和
for(int i=1;i<=n;i++) cin>>sx[i];
for(int i=1;i<=n+1;i++) le[i]=ri[i]=i;//并查集
ans.push(0);
for(int i=n;i>=1;i--){
anss[i]=-ans.top();//记录答案
int now=sx[i];
le[now]--;//该点被添加以后,其所在区间的左端点即为其左边点的左端点
ri[now]++;//该点被添加以后,其所在区间的右端点即为其右边点的右端点
int l=le_to(now)+1,r=ri_to(now)-1;//该点所在区间的左端点和右端点
int val=qzh[r]-qzh[l-1];//该点所在区间的值
ans.push(-val);//加入答案
}
for(int i=1;i<=n;i++) cout<<anss[i]<<"\n";
return 0;
}
T2-区间逆序对(interval)
有一个长度为 \(n\) 的序列 \(a\) 。进行 \(m\) 次询问,每组询问中给定 \(l\) , \(r\),
求有多少组 \(i,j\) 满足 \(l≤i<j≤r,a_i>a_j\) 。
\(1≤n,m≤10^6,1≤l≤r≤n,∀i∈[1,n],1≤a_i≤50\)
非原题,对应难度为绿
- 注意到值域很小,我们考虑与值域有关的做法。
- 设 \(f_i\) 表示 \(𝑎_{1...i}\) 的逆序对个数。询问时只需要计算 \(f_r-f_{l-1}\) 然后减去 \([1,l)\) 与 \([l,r]\) 之间的贡献即可
- 对于每个 \(x (1 \leq x \leq 50)\) 我们只需要求出 \([l,r]\) 中 \(x\) 的个数和 \([1,l)\) 中 \(> x\) 的数的个数,然后用答案减去前者和后者之积
- 通过前缀和可以实现,时间复杂度 \(O((n+m) V)\),其中 \(V\) 为值域
- 具体看代码
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,V=55;
int n,m,a[N];
int cnt[N][V],sum[N];//cnt[i][j]代表前i个位置中有多少个值为j的位置 sum[i]代表前i个位置共有几对逆序对
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
for(int j=1;j<=V;j++) cnt[i][j]=cnt[i-1][j];
cnt[i][a[i]]++;
sum[i]=sum[i-1];
for(int j=a[i]+1;j<=V;j++) sum[i]+=cnt[i][j];//计算前面i个位置有多少个比ai大的数
}
for(int t=1;t<=m;t++){
int l,r;
cin>>l>>r;
int ans=sum[r]-sum[l-1];
int s=0;//用来保存[1,l)中大于x的数的个数
for(int i=V;i>=1;i--){
ans-=s*(cnt[r][i]-cnt[l-1][i]);//ans减去s与[l,r]中等于x的数的个数之积
s+=cnt[l-1][i];//累加[1,l)中大于x的数的个数
}
cout<<ans<<"\n";
}
return 0;
}
T3-01串(string)
给一个长度为 \(n\) 的 \(01\) 串,你有且仅有一种修改操作可以将 \(0\) 变成 \(1\)。
若有连续两个或以上的 \(1\) ,则称这些 \(1\) 为“好”的 \(1\)。
有 \(q\) 次独立的询问,每次询问为在对原串进行 \(k\) 次修改操作下,你最多能得到多少“好”的 \(1\)。
原题,对应难度为绿
- 手摸几组数据,发现可以贪心,分类讨论一下发现以下规律:
01010,此时操作中心的 \(0\) 后变成 \(01110\) ,能对答案产生贡献为 \(3\)10或01,此时操作旁边的 \(0\),能对答案产生贡献为 \(2\)- 类似
1110111,此时操作中间的 \(0\) ,能对答案产生贡献 \(1\)
- 按照上述规律由贡献由大到小贪心即可,注意全是 \(0\) 的情况要特判
code
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
char s[N];
int top,top1;//情况1,2出现的次数
bool a[N],vis[N];
int n,m,sum,ans[N];//sum为0的个数
signed main()
{
cin>>n>>m;
cin>>s;
for(int i=n;i>=1;i--) a[i]=s[i-1]-'0';
int cnt=0;//记录未操作前的答案
for(int i=1;i<=n;i++){
if(a[i]==0) sum++;
if(a[i]!=a[i-1]){
if(cnt>=2&&a[i-1]==1) ans[0]+=cnt;
cnt=0;
}
cnt++;
}
if(cnt>=2&&a[n]==1) ans[0]+=cnt;//考试时没加这行,100->17,痛失rk5
if(sum==n){//特判全是0的情况
for(int i=1;i<=m;i++){
int x;
cin>>x;
if(x>=2) cout<<x<<"\n";
else cout<<0<<"\n";
}
exit(0);
}
for(int i=2;i<=n-1;i++){//记录情况1出现的次数
if(a[i-2]==0&&a[i-1]==1&&a[i]==0&&a[i+1]==1&&a[i+2]==0){
++top;
vis[i-1]=vis[i]=vis[i+1]=1;
i+=3;
}
}
for(int i=1;i<=n;i++) if(a[i-1]==0&&a[i]==1&&a[i+1]==0&&vis[i]==0) ++top1;//记录情况2出现的次数
for(int i=1;i<=sum;i++){
if(i<=top) ans[i]=3;//情况1
else if(i-top<=top1) ans[i]=2;//情况2
else ans[i]=1;//情况3
}
for(int i=1;i<=sum;i++) ans[i]=ans[i-1]+ans[i];//计算最终答案
for(int i=1;i<=m;i++){//输出
int x;
cin>>x;
cout<<ans[x]<<"\n";
}
return 0;
}
Day2
摸底考试,rk6,难度黄绿绿蓝蓝
T1-数字(number)
形式化题面:
给你两个长度为 \(n\) 的序列 \(a\) \(b\),求出 \(\sum \limits ^n_{i=1} \sum \limits ^n_{j=1} f(a_i+b_j)\),其中 \(f(x)\) 代表 \(x\) 的位数
- 将序列 \(b\) 排序,对于每一个 \(a_i\) ,发现序列 \(b\) 与之相加产生的结果位数会逐级递增(即
1与序列1 9 12 101相加后产生结果的位数为1 2 2 3) - 那么我们就求出所有不同位数之间的分界线后再计算答案即可,发现位数序列具有单调性,所以直接通过二分求就行
- 具体做法是目前做到 \(a_i\) 时,对于每一种可能出现的位数 \(k(1 \leq k \leq 10)\),在 \(b\) 序列里二分找到第一个 \(\leq 10^k-a_i\) 的位置,将这个位置-1即为分界线
- 时间复杂度 \(O(n \log n)\)
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
const long long INF=1e11;
int n,ans=0,a[N],b[N];
signed main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
sort(b+1,b+n+1);//排序
b[n+1]=INF;
for(int i=1;i<=n;i++){
int lj=1;
vector<int> anss;//anss[i]代表结果位数第一次大于等于i的位置
anss.push_back(0);
for(int j=2;j<=10;j++){
lj*=10;
int x=lj-a[i];
if(x<=0){
anss.push_back(0);//代表当前结果不可能出现j这个位数
continue;
}
int fd=lower_bound(b+1,b+n+2,x)-b-1;//寻找分界线
anss.push_back(fd);//加入答案
}
anss.push_back(n);
for(int j=1;j<anss.size();j++) ans+=j*(anss[j]-anss[j-1]);//当前分界线与上一个分界线的距离就是对于当前ai,总共有多少个结果为此位数,计算答案
}
cout<<ans;
return 0;
}
T2-卡片(card)
有 \(n\) 张编号为 \(1,...,n\) 的卡片,第 \(i\) 张卡片的正面写着 \(P_i\) ,反面写着 \(Q_i\)
其中, \(P(1...n)\) 和 \(Q(1...n)\) 都是 \((1...n)\) 的一个排列。
请问,从 \(n\) 张卡片中选择若干张卡片的方案中,满足 \((1...n)\) 中的每个数字都必须出现在所选卡片的正面或反面中的至少一张上的方案有多少种?(可以重复)
请输出方案数对 \(998244353\) 取模的结果。
例:其中一个合法方案为选择 \(1,3\) 卡片,此时这些卡片的正面与反面包含着 \((1,2,3)\) 的整个排列
3
1 2 3
2 3 1
- 图论建模题,对于每一个 \(Q_i\) 向 \(P_i\) 连一条边,则可以发现建出来的图是若干个环
- 发现每个环之间互相独立,所以可以单独分开处理后,乘法原理计算总方案数
- 考虑一个环,环上每一条边对应一张卡片,那么边的选择对应着卡片的选择,由此问题转化为了在一条环上选择若干条边,使得每一个点都能被边覆盖的方案数
- 设 \(f_i\) 为环的大小为 \(i\) 时的方案数,暴力打表发现 \(f_1=1,f_2=3,f_3=4,f_4=7...\)
- 由此发现规律 \(f_i=f_{i-1}+f_{i-2}\) (证明看不懂没写XD),预处理后再根据实际每一个环计算即可
- 时间复杂度 \(O(n)\)
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
const int mod=998244353;
int n,p[N],q[N],nxt[N],sx=0,siz[N],top,f[N],ans=1;
bool vis[N];
int dfs(int x){
int now=nxt[x];
int ans=1;
while(now!=x){
vis[now]=1;
ans++;
now=nxt[now];
}
return ans;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>p[i];
for(int i=1;i<=n;i++) cin>>q[i];
for(int i=1;i<=n;i++) nxt[q[i]]=p[i];//连边,nxt[i]代表i的下一个点是什么
for(int i=1;i<=n;i++){
if(!vis[i]){
siz[++top]=dfs(i);//dfs每一个环的大小并记录
sx=max(sx,siz[top]);//代表这些环中大小最大为多少,用来确定预处理范围
}
}
f[1]=1;
f[2]=3;
for(int i=3;i<=sx;i++) f[i]=(f[i-1]%mod+f[i-2]%mod)%mod;//预处理计算
for(int i=1;i<=top;i++){
ans=ans*f[siz[i]]%mod;//乘法原理计算总方案数
ans%=mod;
}
cout<<ans;
return 0;
}
T3-躲避滚石(rock)
小 P 在玩躲避滚石游戏。
游戏中有 \(n\) 条道路,每条道路都有 \(m\) 个单位位置。
在这 \(n\) 条路上,有 \(p\) 个滚石,这些滚石会以每秒走 \(k\) 个单位的速度从第 \(q_i\) 个置向第 \(1\) 个单位位置的方向滚去
当一个滚石滚到第 \(1\) 个单位位置或滚出道路时,我们认为这个滚石被躲避。
现在小 P 可以从这些道路中任何一条的第 \(1\) 个单位位置开始躲避滚石。而在每一秒滚石移动之前,他都有一次移动到相邻道路或不动的机会。
如果小 P 能躲避所有的滚石,那么他就能胜利。
请你告诉小 P 是否存在任意一种能够胜利的躲避方案。
- 一大堆滚石滚起来处理太麻烦了,注意到这些滚石的相对位置不会变。则我们让滚石静止,让小 P 动。
- 则问题就转化为了一个地图移动问题:小 P 在 \(n\) 个道路上移动,每秒会向右移动 \(k\) 格,在移动前可以移动到相邻道路或不动,不能经过滚石所在位置,求是否存在方案
- 可以发现若存在一个路径可以使小 P 从第 \(1\) 个单位移动到第 \(m\) 个单位,则存在方案,用 \(dfs\) 可以解决。判断道路上 \(x\) 到 \(x+k\) 的单位中是否有滚石可以前缀和处理
- 时间复杂度 \(O(nm)\)
code
#include<bits/stdc++.h>
using namespace std;
const int N=3000+5;
int t,n,m,k,p,mapp[N][N];//mapp[i][j]代表第i条道路上第j个位置
bool f;//是否搜到
void dfs(int x,int y){
if(f) return ;
if(y>m){
f=1;
return ;
}
if(x+1<=n&&mapp[x+1][min(y+k,m)]-mapp[x+1][y-1]==0) dfs(x+1,y+k);//向上相邻的道路移动
if(mapp[x][min(y+k,m)]-mapp[x][y-1]==0) dfs(x,y+k);//不动
if(x-1>=1&&mapp[x-1][min(y+k,m)]-mapp[x-1][y-1]==0) dfs(x-1,y+k);//向下相邻的道路移动
}
signed main()
{
cin>>t;
while(t--){
cin>>n>>m>>k>>p;
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) mapp[i][j]=0;
for(int i=1;i<=p;i++){
int x,y;
cin>>x>>y;
mapp[x][y]=1;//读入滚石位置
}
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) mapp[i][j]+=mapp[i][j-1];//前缀和
f=0;
for(int i=1;i<=n;i++){
dfs(i,1);//dfs
if(f){
cout<<"Yes"<<"\n";
break;
}
}
if(!f) cout<<"No"<<"\n";
}
return 0;
}
Day [3,5]
讲贪心构造和基础算法,听不懂
挑了几道能听懂的题和随机几道题切了切
Day3题单
Day4题单
Day5题单
下面是整理的几道题:
P3524 IMP-Party
给定一张大小为 \(n\) 的无向图( \(n\) 为 3 的倍数),保证图中含有一个大小为 $\frac{2}{3} n $ 的团
请找出图中一个大小为 $\frac{1}{3} n $的团并输出具体点集(任意方案即可)
其中 $ (3 \le n \le 3000 )$
- 对于两个节点 \(u,v\) ,若之间无边连接,则 \(u,v\) 有两种情况:其中一个在团里、都不在团里。但是肯定不可能出现 \(u,v\) 都在一个团里的情况
- 因此每次删除这样的 \(u,v\) 都至少能够删除一个不在团里的点,等到删完之后剩下的点一定都在团里,即剩下的点一定构成一个团
- 因此我们可以枚举每一个节点对,若它们之间无边,则删除这两个节点。考虑最坏情况(每次删除都会删掉团里一个点),不在团里的点共 \(\frac{1}{3} n\) 个,则最坏总共删除了 \(\frac{1}{3} n\) 个在团里的点,剩下在团里的点有 \(\frac{1}{3} n\) 个,刚好能够输出答案。
- 时间复杂度为 \(O(n^2)\),可以通过
code
#include<bits/stdc++.h>
using namespace std;
const int N=4000+5;
void faster(){
ios_base::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
}
bool tu[N][N],vis[N];//邻接矩阵存图 这个点是否被删除
int n,m;
signed main()
{
faster();//此题输入量大,必须加快读,不然会T
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
tu[a][b]=tu[b][a]=1;
}
for(int i=1;i<=n;i++){
if(vis[i]) continue;//若这个点被删除,则跳过
for(int j=i+1;j<=n;j++){
if(vis[j]||i==j) continue;
if(!tu[i][j]&&!vis[i]&&!vis[j]) vis[i]=vis[j]=1;//如果这两个点没被删且之间无边,则删除这两个点
}
}
int cnt=0;//记录目前已经输出的答案点集多少
for(int i=1;i<=n;i++){
if(vis[i]==0){
cout<<i<<' ';
cnt++;
if(cnt==n/3) exit(0);
}
}
return 0;
}
Day6
NOIP模拟赛,rk19,掉大大大大大大大大分,彻底输在了不会DP上
T1-质因数分解(factor)
给定 \(q\) 个 \(n\) ,将每个 \(n\) 表示为 \({p_1}^{e_1} {p_2}^{e_2} {p_3}^{e_3} \dots {p_m}^{e_m}\) ( \(p_i\) 为互不相同的质数,\(e_i > 0\))
求 \(\gcd (e_1,e_2 \dots e_m)\).
- 观察到 \(n = k^{d}\),即 \(n\) 一定可以被表示为一个因数(k)的几次方,且对于这个因数的答案是指数(d) (\(25412184=2^3\times3^3\times7^6=(2\times3\times7^2)^3={294}^3\))
- 转换一下,\(\sqrt[d]{n}=k\),\(n\) 为整,则 \(k\) 也为整。于是我们可以枚举 \(d\) ,当开完根后为整数时,则记录答案 \(d\)。
- 由于 \(2^{61}>10^{18}\),因此只需要从 61 开始向下枚举 d 即可, 时间复杂度 $O(q \log^2 n) $,这次输在了不会对n开d次方根上
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5;
int t;
long double a;
int cf(int a,int b){//计算a的b次方
int u=1;
for(int i=1;i<=b;i++) u*=a;
return u;
}
signed main()
{
cin>>t;
while(t--){
cin>>a;
bool f=0;
for(int d=60;d>=1;d--){
if(cf((int)round(pow(a,1.0/d)),d)==a){//pow(a,1/d)是给a开d次方根
//外面要加一层round是要先在double类型下将其近似为最近的一位整数,要不然在转换类型时会莫名其妙的出现误差
cout<<d<<"\n";
break;
}
}
}
return 0;
}
Day7
休息
Day[8,9]
学了可持久化线段树
专门写了个博客总结
Day10
NOIP模拟赛,爆0,唐完了
难度黄黄绿绿,怎么越打越菜了
T1调了2.5h后瞎写了一个T2暴力就结束了,结果全程在赌的T1最后爆零,T2的暴力也挂了,赛后发现T1原来是多测没清空
给了两个教训:
- 一道题的思考用时必须控制在 \([1h,1.5h]\) 内,超过这个用时要么你思路麻烦了要么你假了,不要在错误的道路上越陷越深
- 如果题目有多测那必须在调完题后把样例改一改测试一下多测是否清空
思维太差了,要多做一些CF的题了
T1-A+B问题(plus)
众所周知计算机中用二进制表示数字, 但小 A 的电脑却使用了另一种特殊的表示方式。
与二进制相同, 这种表示方式也是基于一个 \(01\) 序列 \(v\) 。设序列 \(v\) 的长度为 \(n\), 其中 \(v_i\) 下标从 \(0\) 开始。
同时给定一个长度为 \(n\) 的序列 \(sgn\), 其中 \({sgn}_i \in \{1,−1\}\)
该表示方式下, 序列 \(v\) 表示的十进制数为 \(\sum \limits^{n-1}_{i=0} {v_i \centerdot {sgn}_i \centerdot 2^i}\)
现给出序列 \(a\) 和 \(b\), 要求求出表示 \(a+b\) 的序列,
本题数据保证 \(a+b\) 一定可以被表示。
- 抽象模拟,赛场思路过于繁杂,不讲
- 多测没清空导致爆零呜呜呜
抽象代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
const long long INF=1e10;
void faster(){
ios_base::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
}
int n,m,sig[N],a[N],b[N],c[N],o1[N],o2[N];
bool vis[N];
signed main()
{
faster();
cin>>n;
for(int i=0;i<n;i++){
cin>>sig[i];
if(sig[i]==-1) o1[i]=1;
else o2[i]=1;
}
cin>>m;
while(m--){
for(int i=0;i<n;i++){
o1[i]=o2[i]=0;
if(sig[i]==-1) o1[i]=1;
else o2[i]=1;
}
int sum1=0,sum2=0;
for(int i=0;i<n;i++){
cin>>a[i];
sum1+=sig[i]*a[i]*(1ll<<i);
}
for(int i=0;i<n;i++){
cin>>b[i];
sum2+=sig[i]*b[i]*(1ll<<i);
}
int u=sum1+sum2;
bool f1=0;
if(u<0){
f1=1;
u=-u;
}
for(int i=0;i<=n-1;i++){
c[i]=u%2;
vis[i]=1;
u/=2;
}
bool f=0;
if(f1){
for(int i=0;i<=n-1;i++){
if((f+o2[i]+c[i])%2==o1[i]){
if(f+o2[i]+c[i]==2) f=1;
else f=0;
continue;
}
else{
vis[i]=0;
if(o2[i]==1) o2[i]=0;
else o1[i]=0;
if(f+o2[i]+c[i]>=2) f=1;
else f=0;
}
}
}
else{
for(int i=0;i<n;i++){
if((f+o1[i]+c[i])%2==o2[i]){
if(f+o1[i]+c[i]==2) f=1;
else f=0;
continue;
}
else{
vis[i]=0;
if(o1[i]==1) o1[i]=0;
else o2[i]=0;
if(f+o1[i]+c[i]>=2) f=1;
else f=0;
}
}
}
for(int i=0;i<n;i++) cout<<vis[i]<<' ';
cout<<"\n";
}
return 0;
}
T2-最小生成树(tree)
小 A 有一张无向图, 现在他想考考你会不会求最小生成树。
但传统的最小生成树题目中, 图往往十分的稀疏, 这令小 A 十分不满。
于是小 A 定义了一种全新的连边方式, 对于一次连边操作 \((l,r,w)\) , 其表示的意义为
- 对于所有满足 \(l≤u<v≤r\) 的点对 \((u,v)\), 在 \(u,v\) 间连一条边权为 \(w\) 的边
现在小 A 请你求出这张图最小生成树的边权和。
- 首先需要 \(kruskal\) 求最小生成树
- 考虑 \(kruskal\) 的过程,将所有边以 \(w_i\) 为第一关键字由小到大,\(r_i-l_i+1\) 为第二关键字由小到大排序,发现所有被选中的边都是形似 \(i\) 与 \(i+1\) 之间的边,因而推断出来除了这些边其他边都没用
- 因此最小生成树就转化为了一个链的形式,对于这些有用的边( i 与 i+1 之间的边)只取其所有取值中最小的即可,这个最小值可
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
const long long INF=1e10;
struct tree{
int l,r;
int val,tag;
}t[N];
int n,m,a[N];
int bh[N],cnt;//储存a数组对应线段树上的所有叶子结点
void build(int x,int l,int r){
t[x]=(tree){l,r,INF,INF};
if(l==r){
bh[++cnt]=x;
return ;
}
int mid=(l+r)/2;
build(x*2,l,mid);
build(x*2+1,mid+1,r);
return ;
}
void change(int x,int k){
if(t[x].tag<=k) return ;
t[x].val=min(t[x].val,k);
t[x].tag=k;
}
void pushdown(int x){
if(t[x].tag!=INF){
change(x*2,t[x].tag);
change(x*2+1,t[x].tag);
t[x].tag=INF;
}
}
void modify(int x,int ql,int qr,int k){
int l=t[x].l,r=t[x].r;
if(ql<=l&&r<=qr){
change(x,k);
return ;
}
pushdown(x);
int mid=(l+r)/2;
if(ql<=mid) modify(x*2,ql,qr,k);
if(qr>=mid+1) modify(x*2+1,ql,qr,k);
t[x].val=min(t[x*2].val,t[x*2+1].val);
return ;
}
void all_pushdown(int x){
int l=t[x].l,r=t[x].r;
if(l==r) return;
pushdown(x);
all_pushdown(x*2);
all_pushdown(x*2+1);
}
signed main()
{
cin>>n>>m;
build(1,1,n);//建树
for(int i=1;i<=m;i++){
int l,r,x;
cin>>l>>r>>x;
modify(1,l,r-1,x);//对区间[l,r)进行修改
}
int ans=0;
all_pushdown(1);//最后计算答案了要把所有节点的懒标记下放避免计算错误
for(int i=1;i<cnt;i++){
if(t[bh[i]].val==INF){
cout<<-1;
exit(0);
}
else ans+=t[bh[i]].val;
}
cout<<ans;
return 0;
}
Day [11,15]
之后再补
老师老师言和是谁啊
Day 16
NOIP模拟赛,难度绿绿蓝蓝,炸了
开场40min梦游,将T1 T2暴力打掉之后开始硬刚T1,
想出正解打出代码后发现代码出锅,此时距离结束剩40min,已经调试了30min,想到自己这个思路可能假掉于是心态崩了,剩下时间梦游
最终喜提暴力分70pts,赛后20min改掉T1错误代码,精神胜利110pts
做题太少导致的
T1-消除(remove)
给定序列 \(a\)
每次删除长度最长的最左面的一段连续的数字区间,求完全删除序列 \(a\) 的删除次数
- 把区间缩一下,缩成若干个 \((a,b)\) 二元组的形式,代表序列 \(a\) 由若干个 \(a\) 个 \(b\) 连起来组合在一起
- 发现此时这若干的二元组变成了一个数组的形式,再想到题目要求的删除区间之类的操作,考虑把每段连续区间看做一个节点,用双向链表维护(赛时脑残写了3个并查集维护)
- 思考链表中的每个节点应该记录什么,首先 \((a,b)\) 二元组和 前驱 后继 必须记录,其次根据
最左面这个条件则还需要记录这个节点在 \(a\) 中的对应的左端点,每次快速求最大的左边的连续区间则可以通过堆维护 - 具体操作为,开始将所有区间丢进一个大根堆(大小为第一关键字,节点位置为第二关键字)里,每次取出符合条件(未被打上标记)的堆顶,删除堆顶后尝试匹配该堆顶的前驱和后继(尝试合并该区间的左右两个小区间),若颜色不同则失败,否则添加新区间为合并后的区间,给两个小区间打上标记后删除,能够取出合法堆顶的次数即为答案
code
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
struct node{
int pos,sum,col;//区间在链表中的位置 个数 颜色
int pre,nxt,id;//前驱 后继 区间在a中对应位置
}ist[N];//链表
int n,tot,a[N];
bool vis[N];//记录区间是否被删除
priority_queue<node> pq;//堆
bool operator<(node a,node b){
if(a.sum==b.sum) return a.id>b.id;
else return a.sum<b.sum;
}
void inst(int a,int b,int c){//插入 在a a的前驱之间插入数量为b 颜色为c的节点
++tot;
node u=(node){tot,b,c,a,ist[a].nxt,ist[a].id+1};
ist[tot]=u;
ist[ist[a].nxt].pre=tot;
ist[a].nxt=tot;
}
void del(int a){//删除
ist[ist[a].nxt].pre=ist[a].pre;
ist[ist[a].pre].nxt=ist[a].nxt;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int cnt=1;
for(int i=2;i<=n;i++){//将原数组缩为二元组的形式
if(a[i]!=a[i-1]){
++tot;
ist[tot]=(node){tot,cnt,a[i-1],tot-1,0,tot};
ist[ist[tot].pre].nxt=tot;
cnt=0;
}
cnt++;
}
++tot;
ist[tot]=(node){tot,cnt,a[n],tot-1,0,tot};
ist[ist[tot].pre].nxt=tot;
for(int i=1;i<=tot;i++) pq.push(ist[i]);//扔进堆里
int ans=0;
while(pq.size()){
while(pq.size()&&vis[pq.top().pos]) pq.pop();//排除不合法的堆顶
node u=pq.top();
vis[u.pos]=1;
if(u.sum==0&&u.col==0) break;//如果堆顶出现错误的区间,则证明整个a已消完
ans++;//记录答案
int x=ist[u.pos].pre,y=ist[u.pos].nxt;//前驱后继
pq.pop();
del(u.pos);
if(ist[x].col!=ist[y].col) continue;//不能匹配跳过
inst(ist[x].pre,ist[x].sum+ist[y].sum,ist[x].col);//插入合并后的新节点
pq.push(ist[tot]);//将新节点扔进堆里
del(x);//删除
del(y);
vis[x]=1;
vis[y]=1;
}
cout<<ans;
return 0;
}
T3-齿轮(gear)
现有一个传动系统,包含了 \(N\) 个组合齿轮和 \(M\) 个链条。
每一个链条连接了两个组合齿轮 \(u\) 和 \(v\),并提供了一个传动比 \(x:y\),即如果只考虑这两个组合齿轮,编号为 \(u\) 的齿轮转动 \(x\) 圈,编号为 \(v\) 的齿轮会转动 \(y\) 圈。
传动比为正表示若编号为 \(u\) 的齿轮顺时针转动,则编号为 \(v\) 的齿轮也顺时针转动。传动比为负表示若编号为 \(u\) 的齿轮顺时针转动,则编号为 \(v\) 的齿轮会逆时针转动。
若不同链条的传动比不相容,则有些齿轮无法转动。我们希望知道,系统中的这 \(N\) 个组合齿轮能否同时转动。
这个传动系统可能不连通。 也就是说,不保证任意两个齿轮都被链条直接或间接地连接。
原题,个人认为严格小于T1,最大难点在于读懂题,其他纯模拟
- 将齿轮看做点,链条看作边,建图。图不连通则将图分为若干个联通块挨个计算
- 对于每一个联通块,随便从其块中一个点为起点,令这个点转1圈,根据每条边挨个计算每个齿轮此时应该转多少圈,跑dfs,若出现一个齿轮会转的圈数大于一种(如样例2,当一轮转 1 圈时,则二轮转 \(\frac{5}{3}\) 圈。但此时三轮会转 \(\frac{7}{3}\) 圈,根据
2 3 5 -7这个链条得出二轮应该转 \(-\frac{5}{3}\) 圈.与前面二轮转的 \(\frac{5}{3}\) 圈发生矛盾,故不符合),则发生矛盾,说明无法转动,否则可以 - 具体操作为搜寻每一个联通块,从联通块中任意一点为起点,通过假设起点转了1圈开始dfs处理出其他点转了几圈,若目前搜到的一个点已经被计算完毕,则比较这个点目前计算的值与目前dfs出这个点应该填的值是否相等,若不相等即为矛盾,输出
No,否则Yes. - 储存每一个点应该转的圈数时可以通过储存分子分母和用
long double来储存(精度小于 \(10^{-10}\) 即可),时间复杂度 \(O(Tn^2)\)
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
struct edge{
int v;
int nxt;
int o1,o2;
}ed[N];
struct node{
int p,q;
};
int t;
int first[N],en,n,m;
bool f,vis[N];//是否存在矛盾 是否已被计算
node val[N];//每个节点此时应该转动的圈数(分子分母)
int gcd(int a,int b){
if(b==0) return a;
else return gcd(b,a%b);
}
void add_edge(int u,int v,int x,int y){
ed[++en]=(edge){v,first[u],x,y};
first[u]=en;
}
void dfs(int x,int fa){
if(f==0) return ;
vis[x]=1;
for(int i=first[x];i!=0;i=ed[i].nxt){
int d=ed[i].v;
if(d==fa) continue;
int pp=ed[i].o1,qq=ed[i].o2;//该边的传动比
int mpp=val[x].p,mqq=val[x].q;//出点转的圈数
int rrpp=mpp*qq,rrqq=mqq*pp;//根据边和出点计算出的入点应该转的圈数
int rpp=rrpp/gcd(rrpp,rrqq),rqq=rrqq/gcd(rrpp,rrqq);//简化分数
if(val[d].p==0&&val[d].q==0){//如果这个点还没被计算
val[d]=(node){rpp,rqq};
dfs(d,x);
}
else{
if(val[d].p!=rpp||val[d].q!=rqq){//如果过这个点已被计算且原先圈数不符合当前被计算的圈数
f=0;//产生矛盾
return ;
}
}
}
}
signed main()
{
cin>>t;
int lk=t;
while(t--){
en=0;
f=1;
cin>>n>>m;
for(int i=1;i<=n;i++){
val[i]=(node){0,0};
vis[i]=first[i]=0;
}
for(int i=1;i<=m;i++){
int u,v,x,y;
cin>>u>>v>>x>>y;
add_edge(u,v,x,y);
add_edge(v,u,y,x);
}
for(int i=1;i<=n;i++){
if(!vis[i]){
val[i]=(node){1,1};//假定起点转一圈
dfs(i,0);
if(!f) break;
}
}
cout<<"Case #"<<lk-t<<": ";
if(f) cout<<"Yes"<<"\n";
else cout<<"No"<<"\n";
}
return 0;
}

浙公网安备 33010602011771号