Logo KSCD_ 的博客

博客

标签
暂无

ABC371F 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-15 07:51:05

思路

发现由于移动时不能让两人在同一位置,所以所有人的相对顺序不会改变,同时移动时若旁边已经有人,还可能需要带上别的人一起动。

考虑把相邻的人看成一个整块一起移动,初始每个人各为一个块,移动时暴力向目标方向走,接上下一个块就合成一个整块接着走,直到到达终点。

具体维护时可以在 set 中维护 $(p_i,x_i)$ 表示一个从 $p_i$ 开始的整块,此时 $p_i$ 的位置为 $x_i$。每个块的右端点和长度可以结合下一个块的左端点来计算,具体操作还是看代码实现吧。

时间复杂度上由于每次操作只可能从一个整块中分出一个含 $T_i$ 的小块,总块数不超过 $S=N+Q$,合并的次数不超过总块数,所以时间复杂度是 $O(S\log S)$ 的。

代码

#include<iostream>
#include<set>
#define int long long
#define pii pair<int,int>
using namespace std; 
const int N=4e5+10;
const int inf=1e18;
int n,Q,res;
set <pii> s;
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1,x;i<=n;i++) cin>>x,s.insert({i,x});
	s.insert({0,-inf}),s.insert({n+1,inf}),cin>>Q; \/\/ 处理边界 
	while(Q--)
	{
		int p,to; cin>>p>>to;
		pii te=*--s.upper_bound({p,inf}); s.erase(te); \/\/ p 所在的整块 
		int lp=te.first,lx=te.second,nowx=lx+(p-lp),nlp=(*s.upper_bound(te)).first; \/\/ lp 为左端点人的编号,lx 为位置 
		if(to<nowx) \/\/ 向左走 
		{
			if(p+1!=nlp) s.insert({p+1,nowx+1}),nlp=p+1; \/\/ 拆块 
			to-=(p-lp); \/\/转化为左端点移动到的位置 
			while(lx!=to)
			{
				pii tt=*(--s.upper_bound({lp,lx})); \/\/ 左边的块 
				int tp=tt.first,tx=tt.second,lim=tx+(lp-tp),tto=max(to,lim);
				res+=(nlp-lp)*(lx-tto),lx=tto;  
				if(lx==lim) s.erase(tt),to-=(lp-tp),lp=tp,lx=tx; \/\/ 合并 
			}
		}
		else \/\/向右走 
		{
			if(p!=lp) s.insert({lp,lx}),lp=p,lx=nowx; \/\/ 拆块 
			while(lx!=to)
			{
				pii tt=*(s.upper_bound({lp,lx})); \/\/ 右边的块 
				int np=tt.first,nx=tt.second,lim=nx-(np-lp),tto=min(to,lim);
				res+=(np-lp)*(tto-lx),lx=tto;
				if(lx==lim) s.erase(tt); \/\/ 合并 
			}
		}
		s.insert({lp,lx}); \/\/把目前的块重新加入集合 
	}
	cout<<res;
	return 0;
}

【比赛记录】24.09.22

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-23 09:30:29

记录

A 题仿照 QOJ8336 写了个二分,但是由于 $inf$ 精度爆了,同时有超过一半的数据包含 $n=10^{18}$,挂到 $44$。

B 题构造了一个多小时,但是忘记特判 $k=1$,同时有超过一半的数据包含 $k=1$ 且 $\frac nk$ 为奇数,挂到 $38$。

C 题想了主席树 + 二分的做法,还胡出来一个奇怪的矩形求和做法,最后都感觉很难写弃了,写了一个前缀和上二分,但是由于二分上界的 $k$ 写成了 $q$,从 $30$ 挂到 $3$。

D 题没太认真想,直接写了 $40$ 的暴力,最后过了 $53$。(事实上赛后自己认真做想出的做法是假的,只能过 $36$)

所以比赛时边界和特判这种细节需要注意,避免挂分。/ll

题解

A. T386386 谜之阶乘

发现 $20$ 的阶乘已经超过了 $10^{18}$,所以答案的长度不会超过 $20$。考虑枚举答案长度,显然在长度相同的前提下,开头越大,最终的乘积就越大。所以可以二分求出第一个不小于 $n$ 的开头位置,再判断一下是否与 $n$ 相等即可。

另外要防止溢出爆掉,只需要在乘法计算前用 __int128 判断一下,如果超过边界就返回边界即可。时间复杂度大概是 $O(400\log VT)$,但是由于暴力计算乘法会在爆掉时退出,极大地跑不满,可以通过。

#include<iostream>
#define int long long 
#define pii pair<int,int>
#define ll __int128
using namespace std;
const int P=2e18;
int n,ct;
pii ans[30];
bool check(int x,int y)
{
	ll te=x;
	te*=y;
	if(te>P) return true;
	return false;
}
int query(int l,int len)
{
	int res=1;
	for(int i=0;i<len;i++)
	{
		if(check(res,l+i)) return P;
		res*=(l+i);
	}
	return res;
}
void sol()
{
	cin>>n,ct=0;
	if(n==1) {cout<<-1<<'\n'; return;}
	for(int i=20;i>=2;i--)
	{
		int l=1,r=P,res=-1;
		while(l<=r)
		{
			int mid=(l+r)\/2;
			if(query(mid,i)<=n) res=mid,l=mid+1;
			else r=mid-1;
		}
		if(res!=-1&&res!=1&&query(res,i)==n) ans[++ct]={res+i-1,res-1};
	}
	ans[++ct]={n,n-1},cout<<ct<<'\n';
	for(int i=1;i<=ct;i++) cout<<ans[i].first<<' '<<ans[i].second<<'\n';
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int TT; cin>>TT;
	while(TT--) sol();
	return 0;
}

B. T386254 子集

发现可以按照 $k$ 个一组划分成 $m=\frac nk$ 组,每组内等价于一个 $1$ 到 $k$ 的排列,只要把 $m$ 组排列分别分到 $k$ 个组里,使得每个组内的和相等即可。

不难注意到 $m$ 为偶数时只需要正序逆序交替即可,下面讨论 $m$ 为奇数的情况。此时若 $k$ 为偶数,我觉得无解,不会证

$k$ 为奇数时先从 $m$ 个排列中取出三个,第一个从 $1$ 到 $k$,第二个从第 $(\frac{k+1}2+1)$ 组开始从 $1$ 到 $k$,可以发现这样能保证所有组中的和恰好好是一个公差为 $1$ 的等差数列。那么用第三个数补充上就恰为一个排列,之后就转化为 $m$ 为偶数的情况了,不太懂是怎么构造出来的,我是打表 + 枚举找到的。

#include<iostream>
#include<vector> 
#define int long long 
using namespace std;
const int N=1e6+10;
int n,k,m,cnt;
vector <int> res[N];
void sol()
{
	cin>>n>>k,m=n\/k,cnt=0;
	if(k==1) 
	{
		cout<<"Yes\n";
		for(int i=1;i<=n;i++) cout<<i<<' ';
		return;
	}
	for(int i=1;i<=k;i++) res[i].clear();
	if(m==1) {cout<<"No\n"; return;}
	if(m%2)
	{
		if(k%2==0) {cout<<"No\n"; return;}
		for(int i=1;i<=k;i++) cnt++,res[i].push_back(cnt);
		for(int i=0;i<k;i++) cnt++,res[((k+1)\/2+1+i-1)%k+1].push_back(cnt);
		int sum=((k+1)\/2+k)*3;
		for(int i=1;i<=k;i++) res[i].push_back(sum-res[i][0]-res[i][1]);
		m-=3,cnt+=k;
	}
	cout<<"Yes\n";
	for(int i=1;i<=m;i++) 
	{
		if(i%2) for(int j=1;j<=k;j++) cnt++,res[j].push_back(cnt);
		else for(int j=k;j>=1;j--) cnt++,res[j].push_back(cnt);
	}
	for(int i=1;i<=k;i++)
	{
		for(int t:res[i]) cout<<t<<' ';
		cout<<'\n';
	}
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int TT; cin>>TT;
	while(TT--) sol();
	return 0;
}

C. T386330 粉末

发现每个询问本质上是询问 $x$ 位置上的块数第一次到达 $y$ 的操作数,显然可以二分,那么就需要快速求出操作若干次后某个位置上的块数。

同时发现已经落下的方块不会改变,那么可以把所有询问离线下来,在所有操作结束后询问。若答案序号大于询问的序号,说明这个位置是在询问之后才被填上的,在询问时是空气。这样就把所有询问离线下来了。

考虑从小到大枚举所有的 $x$,并维护长为 $q$ 的序列 $s$ 使得 $s_i$ 表示前 $i$ 个操作中给 $x$ 处加的块数。那么对于第 $k$ 个增加操作 $s_k=(l_i,r_i,h_i)$,在枚举到 $l_i$ 时在 $s$ 的 $[k,q]$ 上加 $h_i$,枚举到 $r_i+1$ 时在 $s$ 的 $[k,q]$ 上减 $h_i$,就可以保证这次操作只会影响到 $x\in[l_i,r_i]$。

那么就需要支持区间修改(事实上是后缀修改),可以使用线段树 / 树状数组上二分,也可以在二分中用数据结构单点查询。下面的代码是二分 + 树状数组查询,时间复杂度是 $O(q\log^2 q)$ 的。

代码

#include<iostream>
#include<vector>
#define int long long 
#define pii pair<int,int>
using namespace std;
const int N=1e6+10;
int n,q,res[N];
vector <pii> opt[N],ask[N];
struct bit
{
	int b[N];
	int lowbit(int x) {return x&(-x);}
	void add(int p,int x) {for(;p<=q;p+=lowbit(p)) b[p]+=x;}
	int query(int p) {int tr=0; for(;p;p-=lowbit(p)) tr+=b[p]; return tr;}
}T;
int check(int x)
{
	int l=1,r=q,pos=0;
	while(l<=r)
	{
		int mid=(l+r)\/2;
		if(T.query(mid)>=x) pos=mid,r=mid-1;
		else l=mid+1;
	}
	return pos;
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int k=1;k<=q;k++)
	{
		int op; cin>>op,res[k]=-1;
		if(op==1)
		{
			int l,r,h; cin>>l>>r>>h;
			opt[l].push_back({k,h}),opt[r+1].push_back({k,-h});
		}
		else
		{
			int x,y; cin>>x>>y;
			ask[x].push_back({y,k});
		}
	}
	for(int i=1;i<=n;i++) 
	{
		for(pii te:opt[i]) T.add(te.first,te.second);
		for(pii te:ask[i]) res[te.second]=check(te.first);
	}
	for(int i=1;i<=q;i++) if(res[i]!=-1) cout<<(res[i]>i?0:res[i])<<'\n';
	return 0;
}

D. T386391 排水系统

考虑每条边断掉时的影响,发现边 $(u,v)$ 断掉时其他边一定还存在,所以流入 $u$ 点的水量与没有边断掉时的水量相等。这时只需要处理从点 $u$ 流出去的点接收到的水量即可。

这里设 $s_u$ 表示从 $u$ 点连出去的出边数量,$d_u$ 表示没有边被断掉时流入 $u$ 点的水量,可以先跑一遍拓扑排序预处理出来。

那么边被断掉之前每个从 $u$ 连出去的点都会接收到 $\frac{d_u}{s_u}$ 的水,断掉之后除 $v$ 点外每个点会收到 $\frac{d_u}{s_u-1}$ 的水,增加了 $\frac{d_u}{s_u-1}-\frac{d_u}{s_u}$;$v$ 点不会从 $u$ 点接受到水。

如果暴力修改这些东西,复杂度可以被卡到 $O(k^2)$,不可接受。我们发现只有 $v$ 一个点不同,那么考虑把其他点变多的水量乘上 $s_u$ 放到 $u$ 点上,化简得 $t=s_u(\frac{d_u}{s_u-1}-\frac{d_u}{s_u})=\frac{d_u}{s_u-1}$。这时流向每个点的水量都变成了 $\frac {d_u}{s_u-1}$,那么单独给 $v$ 点减去 $\frac{d_u}{s_u-1}=t$ 消去就行。(这里 $u$ 增加和 $v$ 减少的量相等也保证了水量不变)

需要注意对 $u,v$ 两点的操作建立在边 $(u,v)$ 断掉的基础上,所以这些修改均需要乘上边断掉的概率 $p_j$,也就是说给 $u$ 点加上 $tp_j$,给 $v$ 点加上 $-tp_j$。

把每一条边的贡献累加完后,就相当于把所有情况的变化量带权地放到了节点上。这时再正常给初始节点分别放入 $1$ 的水,跑一遍拓扑排序就得到答案了。时间复杂度 $O(n+k)$。(注意两次拓扑的数组别用混,我因为这个调了好久

代码

#include<iostream>
#include<queue>
#define int long long 
using namespace std;
const int N=1e6+10;
const int mod=998244353;
int po(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1) res=res*a%mod;
		a=a*a%mod,b>>=1; 
	}
	return res;
}
int n,m,R,k,tot,itot,ans[N],res[N],r[N],s[N],tr[N],d[N];
int to[N],a[N],nxt[N],head[N],st[N];
bool f[N]; 
queue <int> q;
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m>>R>>k;
	for(int i=1;i<=k;i++)
	{
		cin>>st[i]>>to[i]>>a[i],tot+=a[i]; 
		r[to[i]]++,s[st[i]]++,nxt[i]=head[st[i]],head[st[i]]=i;
	}
	itot=po(tot%mod,mod-2);
	for(int i=1;i<=n;i++) 
	{
		tr[i]=r[i];
		if(!r[i]) d[i]=1,q.push(i);
		else d[i]=0;
	}
	while(q.size())
	{
		int u=q.front(); q.pop();
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			d[v]=(d[v]+d[u]*po(s[u],mod-2)%mod)%mod,r[v]--;
			if(!r[v]) q.push(v);
		}
	}
	for(int i=1;i<=k;i++)
	{
		int v=to[i],u=st[i],ts=d[u]*po(s[u]-1,mod-2)%mod*a[i]%mod*itot%mod;
		res[u]=(res[u]+ts)%mod,res[v]=(res[v]-ts+mod)%mod;
	}
	for(int i=1;i<=n;i++) 
	{
		r[i]=tr[i];
		if(!r[i]) res[i]++,q.push(i);
	}
	while(q.size())
	{
		int u=q.front(); q.pop();
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			res[v]=(res[v]+res[u]*po(s[u],mod-2)%mod)%mod,r[v]--;
			if(!r[v]) q.push(v);
		}
	}
	for(int i=n-R+1;i<=n;i++) cout<<res[i]<<' ';
	return 0;
}

P11005 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-24 10:38:16

题意

给出一张 $n$ 个点 $m$ 条边的带权无向图。定义路径的权值为路径上所有边权的最大值,设 $f(u,v)$ 表示 $u,v$ 之间的所有路径中最小的路径权值,求满足 $u<v,l\le f(u,v)\le r$ 的二元组 $(u,v)$ 的数量。

思路

考虑转化条件,不难发现 $f(u,v)=x$ 等价于 $u,v$ 不能通过所有边权小于 $x$ 的边连通,而能通过所有边权不大于 $x$ 的边连通。

那么按照边权从小到大加入每一条边,当加入一条边权为 $x$ 的边时,由不连通转为连通的点对即为 $f(u,v)=x$ 的点对。也就是说若这条边连接了两个不同的连通块,大小分别为 $s_p,s_q$,那么这些点两两匹配后就有 $s_ps_q$ 个 $f(u,v)=x$ 的点对。

按边权排序后依次加边,用并查集维护连通块及大小,在 $l\le x\le r$ 时统计答案即可。

代码

#include<iostream>
#include<algorithm>
#define int long long
using namespace std;
const int N=2e5+10;
const int inf=2e18;
int n,m,l,r,res,f[N],s[N];
struct edg {int u,v,w;}e[N];
bool cmp(edg ta,edg tb) {return ta.w<tb.w;}
int finds(int x) {return (f[x]==x?x:f[x]=finds(f[x]));}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m>>l>>r,e[m+1].w=inf;
	for(int i=1;i<=n;i++) f[i]=i,s[i]=1;
	for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w;
	sort(e+1,e+1+m,cmp); int now=1;
	while(e[now].w<=r)
	{
		int fu=finds(e[now].u),fv=finds(e[now].v);
		if(fu!=fv)
		{
			if(e[now].w>=l) res+=s[fu]*s[fv];
			f[fu]=fv,s[fv]+=s[fu];	
		}
		now++;
	}
	cout<<res;
	return 0;
}

P11006 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-24 10:51:42

题意

有多种士兵,每种有若干个,求最小的 $x$ 使得任意选出 $x$ 名士兵均能保证可以选出 $k$ 个三人组,且每个组中的三人种类相同。

思路

转化成逆命题即为:求最大的 $y$ 使得存在一种选出 $y$ 名士兵的方案,在这 $y$ 名士兵中无法选出 $k$ 个种类相同的三人组。那么选出 $(y+1)$ 名士兵时就一定满足题意,所以最小的 $x$ 即为 $y+1$。

考虑怎么计算出最大的 $y$,发现可以先从每种里取 $2$ 名士兵,之后再取 $1$ 名时,若剩余数量足够,直接同时多取 $2$ 名,也就是取 $3$ 名;若剩余数量不够,则一定无法再组成更多三人组,也直接把剩下的全部取出。

这样保证了在剩余士兵中任取一名即可组成一个新的三人组,也就能在保证三人组数量最少的前提下取出尽可能多的士兵。那么能组成 $(k-1)$ 个三人组时取出的士兵数即为要求的 $y$。

实现上可以用 map 存储每个种类的士兵数,当然也可以离散化 + 桶记录。特判无解后每种先取出 $2$ 名,统计剩余足够 $3$ 名士兵的数量 $cnt$,把剩余的 $1,2$ 数量记在 $f_1,f_2$ 中,依次取出即可。

代码

#include<iostream>
#include<map>
#define int long long
using namespace std;
int n,k,sum,cnt,res,f[3];
map <int,int> t;
void sol()
{
	cin>>n>>k,k--,sum=res=cnt=f[1]=f[2]=0,t.clear();
	for(int i=1,x,y;i<=n;i++) cin>>x>>y,t[x]+=y;
	for(auto te:t)
	{
		int x=te.second; sum+=x\/3;
		if(x<=2) res+=x;
		else res+=2,cnt+=((x-2)\/3),f[(x-2)%3]++;
	}
	if(sum<k) {cout<<-1<<'\n'; return;}
	cnt=min(cnt,k),res+=cnt*3,k-=cnt;
	for(int i=2;i>=1;i--)
	{
		int tx=min(f[i],k);
		k-=tx,res+=tx*i;
	}
	cout<<res+1<<'\n';
} 
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int TT; cin>>TT;
	while(TT--) sol();
	return 0;
}

CF1946F 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-24 17:33:27

思路

注意到给出的是一个排列,也就是每个数只会出现一次。所以如果枚举倍数或因数,总复杂度都是 $O(n\ln n)$ 的,这是本题复杂度正确的关键。

考虑对整个序列怎么做,设 $f_i$ 表示以 $i$ 为结尾的合法子序列数量,转移为 $f_i=\sum_{j=1}^{i-1}f_j[a_j\mid a_i]+1$,也就是在结尾是 $a_i$ 因数的子序列后接上 $a_i$,或 $a_i$ 自身作为一个子序列。

再扩展到区间询问。考虑把所有询问离线下来,然后从后往前依次处理整个序列,$f_i$ 也变成结尾为 $i$ 且开头已被处理的子序列数量。那么对于询问 $[l,r]$,只需要在处理完 $l$ 后计算 $\sum_{i=l}^r f_i$ 即为答案。

考虑如何计算 $a_i$ 对后面 $f$ 值的贡献。设 $d_j$ 表示 $f_j$ 在这一次维护中增多的数量,显然后面所有满足 $a_i\mid a_j$ 的 $f_j$ 都可以接在 $a_i$ 后面,也即 $d_j=1$。另外也可以从 $j$ 继续往后接上 $k$,需要满足 $a_j\mid a_k$。所以按照递增顺序依次处理每个 $j$,在处理完 $d_j$ 后给后面所有 $a_j\mid a_k$ 的 $d_k$ 加上 $d_j$,也就是在 $a_i$ 后面接上了一段子序列。

这样处理完所有 $d_j$ 后再全部加入 $f$ 数组即可。可以发现维护 $f$ 数组的过程是单点修改,同时会有对 $[l,r]$ 的区间查询,可以使用树状数组维护。时间复杂度为 $O(n\ln n(\ln n+\log n)+q \log n)$,分别为两重枚举倍数、单重枚举倍数修改和查询的复杂度。

代码

代码中预处理了 $tp_i$ 记录 $i$ 后面所有是 $a_i$ 倍数的位置。

#include<iostream>
#include<algorithm>
#include<vector>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e6+10;
int n,q,d[N],a[N],pos[N],res[N]; 
vector <pii> ask[N]; 
vector <int> tp[N];
struct bit
{
	int b[N];
	void clear() {for(int i=0;i<=n;i++) b[i]=0;}
	int lowbit(int x) {return x&(-x);}
	void add(int p,int k) {for(;p<=n;p+=lowbit(p)) b[p]+=k;}
	int query(int p) {int tr=0; for(;p;p-=lowbit(p)) tr+=b[p]; return tr;}
}f;
void sol()
{
	cin>>n>>q,f.clear();
	for(int i=1;i<=n;i++) cin>>a[i],pos[a[i]]=i,ask[i].clear(),tp[i].clear();
	for(int i=1;i<=n;i++)
	{
		for(int j=a[i]*2;j<=n;j+=a[i]) if(pos[j]>i) tp[i].push_back(pos[j]);
		sort(tp[i].begin(),tp[i].end());
	}
	for(int i=1,l,r;i<=q;i++) cin>>l>>r,ask[l].push_back({r,i});
	for(int i=n;i>=1;i--)
	{
		f.add(i,1);
		for(int j:tp[i]) d[j]=1;
		for(int j:tp[i]) for(int k:tp[j]) d[k]+=d[j];
		for(int j:tp[i]) f.add(j,d[j]);
		for(pii te:ask[i]) res[te.second]=f.query(te.first);
	}
	for(int i=1;i<=q;i++) cout<<res[i]<<' ';
	cout<<'\n';
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int TT; cin>>TT;
	while(TT--) sol();
	return 0;
}

CF1762F 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-28 11:09:43

思路

发现若合法子序列的三个连续元素 $x,y,z$ 满足 $a_x\ge a_y,a_y\le a_z$,那么一定有 $\left| a_z-a_x\right|\le k$,可以删掉中间的 $a_y$,$a_x\le a_y,a_y\ge a_z$ 的也一样。所以所有合法子序列均可通过删除操作变得单调不增或单调不降,因此只考虑单调序列即可。

显然单调不增和单调不降两种序列只会在 $a_l=a_r$ 时重复计算,因此分别计算后减去所有的重复贡献即可。可以先计算单调不增的区间数,然后进行 $a_i\leftarrow P-a_i+1$ 的转化($P$ 为 $a_i$ 的值域),就把原来的单调不降变成了单调不增,再做一遍即可。

考虑怎么处理这个序列,发现对于 $i$ 后面第一个 $a_i\le a_j\le a_i+k$ 的 $j$,如果直接接上这个数,对于后面继续选不小于 $a_j$ 的数一定不劣。那么设 $f_i$ 表示 $l=i$ 时单调不增序列的结尾 $r$ 的个数,可以直接先把 $f_i$ 累加上 $f_j$。

剩下的就只有 $j$ 后面小于 $a_j$ 的数,这些数不能接在 $a_j$ 后面,需要另外统计。根据 $j$ 是第一个合法数的定义,$j$ 前面已经不可能有 $[a_i,a_j]$ 内的数,所以需要另外统计的个数等价于 $i$ 后面在 $[a_i,a_j)$ 范围内数的个数。

现在需要求的是每个数后面的 $j$(代码中为 $nxt_i$),考虑按照 $a$ 的值从大到小处理,用 set 维护目前在 $[x,x+k]$ 范围内的数,求 $nxt$ 时在 set 中二分即可,需要注意 $a$ 值相等的位置要从后往前加入 set,才能在前面计算时找到后面相等的数。另外还需要维护每个数后面在 $[a_i,a_{nxt_i})$ 内的数,这个在倒序处理时维护权值树状数组即可实现。

另外还需要注意所有的清空都需要清空每个 $a_i$ 的贡献,而不是对整个数组清空,否则时间复杂度是假的。

代码

#include<iostream>
#include<vector>
#include<set> 
#include<algorithm>
#define int long long
using namespace std;
const int N=5e5+10;
const int P=1e5;
int n,k,res,a[N],t[N],nxt[N],f[N];
set <int> pos;
vector <int> tp[N],tv; 
struct bit
{
	int b[N];
	int lowbit(int x){return x&(-x);}
	void add(int p,int x){for(;p<=P;p+=lowbit(p)) b[p]+=x;}
	int query(int p){int tr=0; for(;p;p-=lowbit(p)) tr+=b[p]; return tr;} 
}T;
void solv()
{
	for(int i=1;i<=n;i++)
	{
		if(tp[a[i]].empty()) tv.push_back(a[i]);
		tp[a[i]].push_back(i);
	}
	sort(tv.rbegin(),tv.rend()),pos.insert(n+1); int l=0;
	for(int x:tv)
	{
		while(tv[l]>x+k)
		{
			for(int p:tp[tv[l]]) pos.erase(p);
			l++; 
		}
		sort(tp[x].rbegin(),tp[x].rend());
		for(int p:tp[x]) nxt[p]=(*pos.lower_bound(p)),pos.insert(p);
	}
	for(int x:tv) tp[x].clear();
	tv.clear(),pos.clear();
	for(int i=n;i>=1;i--)
	{
		f[i]=1;
		if(nxt[i]!=n+1) f[i]=f[nxt[i]]+T.query(a[nxt[i]]-1)-T.query(a[i]-1)+1;
		T.add(a[i],1),res+=f[i];
	}
	for(int i=1;i<=n;i++) T.add(a[i],-1);
}
void sol()
{
	cin>>n>>k,res=0;
	for(int i=1;i<=n;i++) cin>>a[i],t[a[i]]++,res-=t[a[i]];
	solv();
	for(int i=1;i<=n;i++) t[a[i]]--,a[i]=P-a[i]+1;
	solv(),cout<<res<<'\n';
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int TT; cin>>TT;
	while(TT--) sol();
	return 0;
}

CSP-S2024 游记

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-10-27 21:40:52

跨越山川终已至,定数无可避。

  • Day-1

教练在题单里放了 20 多个板子,写爽了。

晚饭前送来了一个大蛋糕,机房一起全切了。

晚上找到了今年三月调了五六个小时才过的序列合并,重写半个小时过了,代码还从 3k 压到 2k,赢。(但是跟我一起写的 zibenlun 对着 5k 代码一直没调出来)

  • Day0

上午在机房随便写了点奇怪的东西(比如用 Splay 写了线段树 1),水了点水题。

中午吃完午饭出发,路上一直在跟 zibenlun 一起听,睡了一觉,打了会集成战略。到酒店以后颓。

晚上试机,考场在 803,爬楼梯上去。但是键盘很好用,随便写了写线段树和 Tarjan 就跑了。因为没啥很熟的同学 + 社恐,没有面基。

回酒店一群人聚到一个屋准备开颓,然后被教练抓了。之后五六个人在我和 zibenlun 屋,点了蜜雪,四个人打了会集成战略,打完集就睡了。

  • Day1

吃了早饭就开始看各种考前提醒,写了模拟退火和对拍。一群人聚到我们屋,一块在电视上乱看,最后在看奶龙的时候被教练把电视关了。中午吃完饭睡了一会,在酒店门口拍了个合照就去考场了。

开场发现 T1 水,很快写完了。T2 仔细想了想感觉也不难,45 min 的时候写完,大样例跑了 1.1 秒。看到大样例只有一半是极限数据,开始卡常。推了个式子替掉了二分,还试了用 vector 桶排代替快排,但变慢了所以没采用。最终优化到 0.8 秒。

这时大概开场 80 min,开了 T3。由于自己很菜并且往年题不简单,所以觉得 T3 肯定切不了,直接先写了 $O(TnV)$ 的 DP。结果仔细一看怎么这么好优化,敲了个线段树优化到 $O(Tn\log V)$。写之前算复杂度还算错了,以为是 4e8,但是觉得反正线段树好写,写了再说。写出来测完样例重新算了一遍才发现是 4e7,感觉能过。

此时也就开场 2h 多点,开始看 T4,这是我未曾设想的速度。读懂题以后感觉这难度不是我能做的,直接开写 $n,m\le8$ 暴力,写完调对大概是 3h。这时我觉得 T4 太困难就直接弃了,其实 A 性质好像并不困难,但我没看。赛后发现我的暴力在 A 性质下是 $O(n^2)$ 的,或许还能多过一点。

弃掉 T4 后就回去检查了,毕竟从未场切过这么多题,这回还是求稳了。重读了代码,确认了格式,重测了样例,在 3.5h 的时候整完了。最后把 T2T3 的代码拿出来写对拍,T2 用二分和式子拍,T3 用暴力和线段树拍,一直拍到结束也没拍出错。也可能是数据造水了之类,不管了。

如果不挂应该是 $[312,320]$,比去年只会暴力好多了。赛后看洛谷发现 T4 是黑,这下没冲也没啥遗憾了。

返程一直在刷 B 站,八点开始看明日方舟前瞻直播,不过此时已经有同学开始在大巴上打 ABC 了。

  • Day3(?)

把前三题代码复刻了一遍,交了洛谷和云斗,结果 T3 线段树被卡 T 了。然后发现线段树就求了个前后缀最大值,改成树状数组全过了并且跑得很快,而且直接维护全局最大值也不会影响答案,寄麻了。T3 变成 $[75,100]$,总分变成 $[287,320]$。

没了,希望出分不挂,希望一个月后的 NOIP 顺利。

声闻形貌终已远,变数未可知。

【比赛记录】24.10.29

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-10-30 20:33:48

记录

A 一个 $n$ 写成 $m$ 挂 $10$ 分,C 的 $n=m$ 写了但是后来写前面的暴力时改小了数组,挂 $8$ 分。

题解

A. T392580 palin

考虑从左上角和右下角向中间 DP,显然可以设 $f_{i,x,y,X,Y}$ 表示步数和两个结尾的坐标,但是只有步数合法的坐标才有意义,这样会有很多无用状态。

所以设 $f_{i,j,k}$ 表示两边各走了 $i$ 步,左上角出发向下 $j$ 步,向右 $(i-j)$ 步,右下角出发向上 $k$ 步,向左 $(i-k)$ 步,且已走的路径两边回文的方案数。此时即从左上走到了 $(j+1,i-j+1)$,从右下走到了 $(n-k,m-i+k)$,先判断一下这两点是否相同,转移时枚举两边分别是竖着走还是横着走即可。

一共各走 $\lfloor\frac {n+m-2}2\rfloor$ 步。统计答案时,若 $n+m-2$ 为偶数,则两边最终会走到同一点,直接枚举走到的点。否则两边会走到距离恰为 $1$ 的两点,分一共竖着走了 $(n-1)$ 和 $(n-2)$ 步分别枚举即可。时间复杂度 $O(n^3)$。

#include<iostream>
using namespace std;
const int N=5e2+10;
const int mod=993244853;
void add(int &a,int b){a+=b;if(a>=mod)a-=mod;}
int n,m,res,st,f[2][N][N];
char a[N][N];
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m,st=(n+m-2)\/2;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		for(int j=m;j>=1;j--) a[i][j]=a[i][j-1];
	}
	if(a[1][1]!=a[n][m]) {cout<<0; return 0;}
	f[0][0][0]=1;
	for(int i=1;i<=st;i++)
	{
		int now=i%2,last=1-now;
		for(int j=0;j<=i;j++) for(int k=0;k<=i;k++)
		{
			f[now][j][k]=0;
			if(j>=n||k>=n||i-j>=m||i-k>=m) continue; 
			if(a[j+1][i-j+1]!=a[n-k][m-(i-k)]) continue;
			if(j&&k) add(f[now][j][k],f[last][j-1][k-1]);
			if(j&&i-k) add(f[now][j][k],f[last][j-1][k]);
			if(k&&i-j) add(f[now][j][k],f[last][j][k-1]);
			if(i-j&&i-k) add(f[now][j][k],f[last][j][k]);
		}
	}
	if((n+m-2)%2) for(int i=0;i<n;i++)
	{
		add(res,f[st%2][i][n-1-i]);
		if(n-2-i>=0) add(res,f[st%2][i][n-2-i]);
	}
	else for(int i=0;i<n;i++) add(res,f[st%2][i][n-1-i]);
	cout<<res;
	return 0;
}

B. T392683 Qsort

模拟一下发现,若 nan 作为基准值,则所有的 $a_l<a_i$ 均为假,没有数会被放到该 nan 前面,也即整个数组都不会变。若某个数 $x$ 作为基准值,则所有小于 $x$ 的数会被一起放到 $x$ 前面,其他的数和 nan 仍留在后面。不难发现此时前面的区间里只有数,因此最终一定会被排好序。

因此从左到右处理每个数,若已经被其他的数排序时提前,或者该数是 nan 则跳过,否则将后面所有小于 $x$ 的数升序放在 $x$ 前,最后放入 $x$。用优先队列可以实现该过程,时间复杂度 $O(n\log n)$。

#include<iostream>
#include<vector>
#include<queue>
#define pii pair<int,int>
using namespace std;
const int N=5e5+10;
int n,ct,a[N],res[N];
bool vis[N];
string ts;
int getn()
{
	int len=ts.size(),tr=0;
	for(int i=0;i<len;i++) tr=tr*10+ts[i]-'0';
	return tr;
}
priority_queue <pii,vector<pii>,greater<pii> > q;
void sol()
{
	cin>>n,ct=0;
	while(q.size()) q.pop();
	for(int i=1;i<=n;i++) cin>>ts,a[i]=(ts[0]=='n'?0:getn()),vis[i]=0;
	for(int i=1;i<=n;i++) if(a[i]) q.push({a[i],i});
	for(int i=1;i<=n;i++) if(!vis[i])
	{
		if(!a[i]) {res[++ct]=a[i]; continue;}
		while(q.size())
		{
			pii te=q.top(); int x=te.first,y=te.second;
			if(x>=a[i]) break;
			if(y<=i) {q.pop(); continue;}
			vis[y]=1,res[++ct]=a[y],q.pop();
		}
		res[++ct]=a[i];
	}
	for(int i=1;i<=ct;i++)
	{
		if(res[i]) cout<<res[i]<<' ';
		else cout<<"nan ";
	}
	cout<<'\n';
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int TT; cin>>TT;
	while(TT--) sol();
	return 0;
}

C. T392686 chaoticevil

首先若 $n$ 为奇数,添加 $a_{n+1}=0$ 变成偶数情况。

考虑如果给 $x$ 赋 $1$,给 $y$ 赋 $-1$,相当于给最终结果加上 $x-y$ 或减去 $y-x$,也就是说给两数赋上不同系数时,相当于只加或减了两数的差值。

因此我们将 $a$ 数组排序,并转化为 $b_i=a_{2i}-a_{2i-1}$,则 $b$ 数组长度 $k=\frac n2$。此时由于 $a$ 中每个数在 $\sum b$ 中都有贡献,只是有些取反了,所以 $\sum b$ 奇偶性不变,仍是偶数。同时由于原数两两不同,$b$ 数组中也没有 $0$。若能构造出一组 $e_i$ 使得 $\sum e_ib_i=0$,就可以构造出一组原问题的解。

$a$ 和 $b$ 似乎一样,但是后者有更强的性质,有 $\sum b\le m-\frac n2$。证明的话由于 $a_i\in [0,m]$,极差不超过 $m$,同时有 $\frac n2$ 个 $a_{2i+1}-a_{ai}$ 的空没有选,这些空的和至少为 $\frac n2$。

由于原题中保证 $\frac {2m}3<n$,我们取 $n=\frac {2m}3$ 代入上式,也即 $m-\frac n2<m-\frac m3=\frac {2m}3<n$,有 $\sum b<n=2k$。我们得到的新问题即为:给出长为 $k$,元素非 $0$ 的数组 $b$,满足 $\sum b<2k$ 且为偶数,构造一组 $e_i$ 使得 $\sum e_ib_i=0$。

不难想到如果 $b$ 中是偶数个 $1$,就可以给一半赋为 $-1$ 使得最终和为 $0$。如果有大于 $1$ 的数,考虑像最开始的转换一样再次转化,取出 $\max b$ 和 $\min b$,并加入它们的差值。由于 $\sum b<2k$,最大值和最小值此时不相等,因此差值仍为正。 $\sum b$ 减少了 $2\min b$,至少减少了 $2$ ,保证新的 $\sum b'<2k'$ 且仍为偶数,这样直到 $b$ 中数字全为 $1$ 即得解。

因此我们证明了在原问题条件下一定有解,并可以给出一组解。具体实现上可以使用优先队列维护目前的最值,并记录每个元素的来源(是哪两个元素相减所得),最终从所有的 $\pm 1$ 开始 dfs 一遍即可得到答案。时间复杂度 $O(n\log n)$。

#include<iostream>
#include<queue>
#include<algorithm>
#define pii pair<int,int>
using namespace std;
const int N=1e6+10;
int n,m,ct,a[N],v[N],c[N],lc[N],rc[N],p[N];
bool vis[N];
priority_queue <pii> qi,qa;
bool cmpa(int x,int y){return a[x]<a[y];}
void dfs(int x,int k)
{
	if(x<=0) {c[-x]=k; return;}
	dfs(lc[x],-k),dfs(rc[x],k);
}
void solv()
{
	for(int i=1;i<=ct;i++) qi.push({-v[i],i}),qa.push({v[i],i});
	while(qa.size())
	{
		int x=qa.top().second; qa.pop();
		while(vis[x]) x=qa.top().second,qa.pop();
		if(v[x]==1)
		{
			int k=1;
			for(int i=1;i<=ct;i++) if(!vis[i]) dfs(i,k),k=-k;
			return;
		}
		int y=qi.top().second; qi.pop();
		while(vis[y]) y=qi.top().second,qi.pop();
		ct++,vis[x]=vis[y]=1,lc[ct]=y,rc[ct]=x,v[ct]=v[x]-v[y];
		qi.push({-v[ct],ct}),qa.push({v[ct],ct});
	}
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],p[i]=i;
	sort(p+1,p+1+n,cmpa);
	for(int i=n;i>0;i-=2) ct++,v[ct]=a[p[i]]-a[p[i-1]],lc[ct]=-p[i-1],rc[ct]=-p[i];
	cout<<"NP-Hard solved\n",solv();
	for(int i=1;i<=n;i++) cout<<c[i]<<' ';
	return 0;
}

D. T392687 pigeons

发现一个点 $u$ 被区间 $[L,R]$ 选中当且仅当该点被区间完全包含,而该点的父亲节点不被完全包含,也即 $L\le l_u\le r_u\le R$ 成立且 $L\le l_{fa_u}\le r_{fa_u}\le R$ 不成立。另外由于 $u$ 被完全包含,所以其父亲区间跟 $[L,R]$ 一定有交。

所以我们考虑用 $L-1$ 和 $R+1$ 来刻画限制。具体地,我们发现根节点到 $L-1$ 的链上的点一定包含 $L-1$,那么它们的右子节点若不在链上,则一定不包含 $L-1$,且表示的区间左端点 $\ge L$,$R+1$ 到根节点的路径同理。

那么我们先找到 $L-1$ 和 $R+1$ 的 LCA $x$,则 $lc_x$ 到 $L-1$ 的链和 $rc_x$ 到 $R+1$ 这两条链不合法,且前者的不在链上的右儿子和后者不在链上的左儿子均合法,即在 $[L,R]$ 区间内。所以我们对这两条链进行操作,以他们的某个儿子所代表的区间长度为系数加上 $d$ 即可。

因此考虑树链剖分,对每一条重链维护其相邻的右子节点和左子节点的值,分在两棵线段树里维护即可。这样我们就可以把每条链剖成不超过 $\log n$ 条重链和轻边,那么我们用线段树维护重链的区间加,另外维护轻边的贡献。由于每个点只会连一条轻边,直接开数组维护每个点上轻边的值即可。时间复杂度 $O(n\log^2 n)$

#include<iostream>
#define int long long
using namespace std;
const int N=8e5+10;
int n,m,q,rt,ct,sr,ch[N][2],fa[N],w[N],aa[N][2];
int son[N],top[N],de[N],siz[N],id[N],ta[N];
struct sgmtt
{
	int t=1,lc[N],rc[N],le[N],s[N],tag[N];
	void pushup(int u) {s[u]=s[lc[u]]+s[rc[u]];}
	void pt(int u,int k) {s[u]+=k*le[u],tag[u]+=k;}
	void pushdown(int u) {if(tag[u]) pt(lc[u],tag[u]),pt(rc[u],tag[u]),tag[u]=0;}
	void build(int u,int l,int r,bool tf)
	{
		if(l==r) {le[u]=aa[l][tf]; return;}
		int mid=(l+r)\/2;
		lc[u]=++t,build(t,l,mid,tf);
		rc[u]=++t,build(t,mid+1,r,tf);
		pushup(u),le[u]=le[lc[u]]+le[rc[u]];
	}
	void update(int u,int l,int r,int ll,int rr,int k)
	{
		if(l>=ll&&r<=rr) {pt(u,k); return;}
		int mid=(l+r)\/2; pushdown(u);
		if(ll<=mid) update(lc[u],l,mid,ll,rr,k);
		if(rr>mid) update(rc[u],mid+1,r,ll,rr,k);
		pushup(u);
	}
	int query(int u,int l,int r,int ll,int rr)
	{
		if(l>=ll&&r<=rr) return s[u];
		int mid=(l+r)\/2,res=0; pushdown(u);
		if(ll<=mid) res+=query(lc[u],l,mid,ll,rr);
		if(rr>mid) res+=query(rc[u],mid+1,r,ll,rr);
		return res;
	}
}T[2];
void dfs(int u)
{
	siz[u]=1,de[u]=de[fa[u]]+1;
	if(u<=n) {w[u]=1; return;}
	for(int i=0;i<2;i++)
	{
		int c=ch[u][i]; dfs(c),w[u]+=w[c],siz[u]+=siz[c];
		if(siz[c]>siz[son[u]]) son[u]=c;
	}
}
void dfsb(int u,int to)
{
	id[u]=++ct,top[u]=to;
	if(!son[u]) return;
	dfsb(son[u],to);
	for(int i=0;i<2;i++)
	{
		int c=ch[u][i];
		if(c!=son[u]) dfsb(c,c),aa[id[u]][i]=w[c];
	}
}
void update(bool tf,int l,int r,int k) {T[tf].update(1,1,m,l,r,k);}
int query(bool tf,int l,int r) {return T[tf].query(1,1,m,l,r);}
int flca(int x,int y)
{
	if(x<0||y>n) return -1;
	while(top[x]!=top[y])
	{
		if(de[top[x]]<de[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	if(de[x]>de[y]) swap(x,y);
	return x;
}
void add(int x,int to,int k,bool tf)
{
	while(top[x]!=top[to])
	{
		if(x!=top[x]) update(tf,id[top[x]],id[fa[x]],k),x=top[x];
		if(x!=ch[fa[x]][tf]) ta[x]+=k;
		x=fa[x];
	}
	if(x!=to) update(tf,id[to],id[fa[x]],k);
}
int ask(int x,int to,bool tf)
{
	int res=0;
	while(top[x]!=top[to])
	{
		if(x!=top[x]) res+=query(tf,id[top[x]],id[fa[x]]),x=top[x];
		if(x!=ch[fa[x]][tf]) res+=ta[x]*w[ch[fa[x]][tf]];
		x=fa[x];
	}
	if(x!=to) res+=query(tf,id[to],id[fa[x]]);
	return res;
}
void opa(int l,int r,int lca,int x)
{
	if(l==1&&r==n) sr+=x*n;
	else if(l==1) add(r+1,rt,x,0);
	else if(r==n) add(l-1,rt,x,1);
	else add(l-1,ch[lca][0],x,1),add(r+1,ch[lca][1],x,0);
}
int opb(int l,int r,int lca)
{
	if(l==1&&r==n) return sr;
	if(l==1) return ask(r+1,rt,0);
	if(r==n) return ask(l-1,rt,1);
	return ask(l-1,ch[lca][0],1)+ask(r+1,ch[lca][1],0);
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>q,m=n*2-1;
	for(int i=n+1;i<=m;i++) cin>>ch[i][0]>>ch[i][1],fa[ch[i][0]]=fa[ch[i][1]]=i;
	for(int i=n+1;i<=m;i++) if(!fa[i]) rt=i;
	dfs(rt),dfsb(rt,rt),T[0].build(1,1,m,0),T[1].build(1,1,m,1);
	while(q--)
	{
		int op,l,r,x,lca; cin>>op>>l>>r,lca=flca(l-1,r+1);
		if(op==1) cin>>x,opa(l,r,lca,x);
		else cout<<opb(l,r,lca)<<'\n';
	}
	return 0;
}

【比赛记录】2024-2025 ICPC, NERC 线上赛

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-11-19 07:34:09

第一次打 ICPC,起因是 wjr 上周说要疯狂星期四一起打,然而比赛从上周四推迟到这周一了。

队员是 wjr、wcz 和我,队名是 維多利亞皇家近衛學院附屬高中陌域坍縮部,是我从明日方舟和 B 站主播那借鉴过来的。同学说这个名字很 ICPC。

开场前在机房里调了调位置,跟 wcz 坐一块了,还和他开了一起听。wjr 不在机房,只能 QQ 联系。

开场做签到题,他们两个还因为沟通不畅都做了 N。我从开场就在读 A 的长题面,然后做了。之后 wjr 和 wcz 切了 L,差不多就把水题做完了。

然后我开了 B,wjr 在 G,wcz 在 K。wjr 好像把 G 的神秘性质整出来了,调了几发过了。之后 wcz 写了 K 的神秘做法,类似于沿着左边界走到最后 $10$ 行然后暴力 DP,但是挂掉了。我听了听思路发现他没考虑沿着上边界走,让他把 $a,b$ 换过来再跑一遍。他最终改成暴力跑 $50$ 行过了,然后作为乱搞大神的他直接去冲 M 去了。

但是我自己开的 B 完全没进展。我一直在想把原序列变成差分序列然后断环为链,要求就是清零。但是发现修改变成三个数还是很不好做。之后切完 G 的 wjr 过来看 B,跟他讨论以后放弃了差分并且开始推二分,最后我只贡献了存在位置不会被加/减的结论,还是 wjr 切掉的。

wjr 想明白并且写 B 的时候我就去看了看 D,列了个暴力 $O(n^2V)$ 的式子,发现转移要求形如 $w_{j,i}\le w_{i+1,k}$,这里 $i$ 固定。想了一会发现端点固定的时候随着区间变长,$w$ 一定是不降的,而且每次变大只会多至少一个 $1$,也就是说只有 $\log V$ 种不同的 $w$ 值,这样后面就可以用前面的一段前缀转移过来。用树状数组可以做到 $O(n\log n\log V)$,感觉很对啊。

想到这的时候是 21:10,wjr 已经切掉 B 了。我因为住校需要十点前回宿舍,于是开始狂暴写码。21:45 写完但是样例过不了,只能把码扔给 wjr,然后在去宿舍的路上把思路告诉了 wcz,期待他们能帮我调出来。

今早上很早就来了机房看 QQ 群,发现昨晚 wjr 还在想 I 题,没想出来之后来调我的码,把我代码里初始化和取模的细节问题改了,在 4:52 的时候极限过掉了。最终 9 题,排名 104,由于罚时太多排在了同题数末尾。

感觉打得很爽啊,或许 ICPC 就是这样,每个人都有自己擅长的部分,比如 wcz 很会猜一些奇怪结论,wjr 能帮我调码 找到性质以后能一直推下去并且想到实现,然后大家一起切题,感觉这种团队合作很有意思。

或许唯一有点遗憾的就是我打到一半跑了,如果我自己调我的 D,wjr 或许就有时间切 I 题了。不过还是很开心,有机会的话还想打。

NOIP2024 游记

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-12-06 20:42:35

所见所知所觅求,幽暗昏黑中

地上炬火借白日,照临寒冬夜

  • Day-2

wzsss2333 在机房打扫雷被围观,然后全机房都开始打了。

教练请喝奶茶,爽喝。

  • Day-1

正常打板子。下午教练开班会,总结了一下过去的一年。

然后有可乐和 KFC,爽食。

  • Day0

上午打了点板子,报上了 THUPC。

中午出发,路上开了个多人一起听。到酒店以后十多个人聚到了一个家庭房里,一半打王者一半打 CS,我在扫雷。

吃晚饭在餐桌上全程搞餐桌礼仪并用方言交流,疯狂爆典。

吃完饭回家庭房,给 ryp 录了神秘小视频

然后就去试机了。机器还不错,但还是由于认识的不多并且社恐,没有面基。

回酒店以后很快各自散了,Iceturky 去我和 zibenlun 屋打了会板子。之后我随便打了会扫雷就睡了。

  • Day 1

吃完早饭就去考场了,带了三块巧克力。然而入场后不让操作机器,所以开场前就吃了一块。

开场以后先读了一遍题,俩计数。回来看 T1 发现好像是神秘贪心,实现起来也不太困难,直接写了,写完的时候大概 40 min。

然后去看 T2,感觉好像 DP 啊,列个式子试试,于是 $f_{i,j}$ 表示第 $i$ 个元素的限制为 $j$ 的方案数。然后推转移,发现 $n^3$ 的转移怎么这么多废状态,直接压成 $f_{i,0/1}$。哦好像可以矩阵优化,开写开写。常数有点大啊,把矩阵拆成俩数转!怎么还有奇怪边界,我调调调。中途还列错了两次式子,最后调过的时候已经 2.5h 了。

然后看后面俩题,感觉好困难啊,直接暴力得了。于是把所有部分列了一下,然后直接去写了 T4 的暴力,拿到 $32$。写完大概还有 1h,就去看 T3 有啥能拿的,然后慢慢把三个暴力全拼上拿了 $40$。

还剩 20 min 了,T4 的链好像有点说法,但是肯定写不完了,检查检查算了。最后期望得分 $100+100+40+32=272$。

出场以后听说曹神阿克了,膜拜。跟 Iceturky 对了对发现我们写的部分分都一模一样。问了问 T1 大家好像都贪心,只有 zibenlun 写了神奇的分块优化 DP,果然是块神。

然后一起去吃淄博烧烤,好吃。饭桌上 chb 还说 T1 贪心假了,说能 hack 啥的,不懂。

返程路上得知 WC 的线是 $316$,差一点,有点怄火但是还是接受了,无所谓了。到家以后爽睡,晚上起来上了号爸课,然而并没怎么听进去。之后打 CF,C 题吃了两发以后不想打了,开始扫雷。喜提 -112。

  • Day2

上午爽睡爽颓。下午回学校,决定先回归一下文化课,等成绩出了再看冲不冲省选。开始猛猛补课。

  • Day4 or Day5

晚上在教室听完听力回机房,同学在说 T2 的判无解,说是提前退出和输出多次 $0$ 大样例都能过。我忘记自己是不是输出了多次,有点慌,但感觉应该是写对了。

  • Day7

突然改到了 12.6 查分,在 15:40 通过申诉网址看到了,没挂分。云斗跑出来并列 rk25-41,高中生 rk20-30。感觉有点超出预期,或许能冲冲试试。这下要希望省选顺利了。

引弓捕影风声切,逐猎无终日

尖牙利爪伺时机,危途知胆识

共 137 篇博客