Logo KSCD_ 的博客

博客

标签
暂无

蝶恋花(互测赛)题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-08-21 19:26:55

补题题号:U468736 - U468741


A.声渐悄

Subtask.1

博弈相关暴搜,或许需要子集枚举之类的东西,我没研究。

Subtask.3

考虑先手如何取最优,发现可以构造使得剩余的物品两两相邻,这样后手选择时每两个就必定会剩下一个,下一轮自己就可以选。按 $n$ 对 $3$ 取模的结果讨论发现答案为 $2\lfloor\frac n3\rfloor+[n \bmod 3\ne0]$。

最难做

我们先假设第一轮先手剩下一段长为 $3$ 的连续段 $a,b,c$ 不取。那么若 $b\ge a+c$,则直接取走 $b$ 一定更优;否则 $b<a+c$,后手一定会先取 $a+c$,这样自己下一次取时也会取走 $b$,第一轮就取走 $b$ 整体不劣。

所以先手可以在剩余连续段长度不超过 $2$ 的前提下达到最优,因此进行 DP,设 $f_i$ 表示先手第一轮取第 $i$ 个时,前 $i$ 个中先手能够获得的最大收益。转移为 $f_i=\max(f_{i-2},f_{i-3}+\min(a_{i-2},a_{i-1}))+a_i$。

意义即为中间留下一个则必为后手的,留两个的话后手会取较大的,自己可以获得较小的。答案为 $\max(f_n,f_{n-1})$,处理一下边界即可通过,时空复杂度 $O(n)$。

n=read();
for(int i=1;i<=n;i++) f[i]=a[i]=read();
for(int i=3;i<=n;i++) f[i]=max(f[i-2],f[i-3]+min(a[i-2],a[i-1]))+a[i];
print(max(f[n],f[n-1]));

B.乱红飞

Subtask.1

把 $2^n$ 个点全建出来,直接树形 DP。设 $f_u$ 表示以 $u$ 为根的子树中包含 $u$ 的连通块种类数,那么 $f_u=\prod_{v\in son_u}(f_v+1)$,也就是每个子树中选连通块的 $f_v$ 种方案和不选的 $1$ 种方案,乘法原理乘起来即可,时空复杂度 $O(2^d)$。代码我没写。

Subtask.2

发现对于满二叉树,同一层的点的子树结构完全相同,所以设 $g_i$ 表示第 $i$ 层点的 $f$ 值,由于都有两个儿子,有 $g_i=(g_{i+1}+1)^2$,答案为 $g_1$。代码我没写,但是离正解已经很近了。

最难做

注:这里 $s$ 字符串的下标为 $[0,d-1]$。

同 Subtask.2 一样,考虑把同一层的点按结构分类。这里按照该子树在第 $d$ 层的点的情况分类,设第 $0$ 类为第 $d$ 层无结点的点,第 $1$ 类为第 $d$ 层结点全满的点,第 $2$ 类为第 $d$ 层部分有结点的点。显然每层最多只有一个第 $2$ 类点。

那么设 $g_{i,j},j\in[0,2]$ 表示第 $i$ 层第 $j$ 种点的 $f$ 值,边界为 $g_{d,1}=1。$显然当 $j\le 1$ 时有 $g_{i,j}=(g_{i+1,j}+1)^2$,问题在于 $g_{i,2}$ 是怎么来的。

考虑算出第 $i$ 层的第 $2$ 类点在第 $d$ 层的点数,发现这个数量是 $s$ 的第 $i$ 位直到结尾所表示的二进制数。那么从后往前第一个 $1$ 处就是首次出现第 $2$ 类点的层数。

在出现第 $2$ 类点后,若 $s_i=1$,说明上一层的 $2$ 类点并上了一个 $1$ 类点,$s_i=0$ 说明并上了一个 $0$ 类点。因此有 $g_{i,2}=(g_{i+1,2}+1)(g_{i+1,s_i}+1)$,注意最终答案还要分讨一下。时空复杂度 $O(d)$。

cin>>d>>s,g[d][1]=1; bool tf=false;
for(int i=d-1;i>=1;i--)
{
	g[i][0]=(g[i+1][0]+1)*(g[i+1][0]+1)%mod,g[i][1]=(g[i+1][1]+1)*(g[i+1][1]+1)%mod;
	if(tf) g[i][2]=(g[i+1][2]+1)*(g[i+1][s[i]-'0']+1)%mod;
	else if(s[i]-'0') tf=true,g[i][2]=(g[i+1][1]+1)*(g[i+1][0]+1)%mod;
}
if(s[0]=='1') cout<<g[1][1]<<'\n';
else if(tf) cout<<g[1][2]<<'\n';
else cout<<g[1][0]<<'\n';

C.恨千缕

Subtask.1.2

考虑暴力,也就是记录下每缕思绪是否存在以及出现的时间,每次查询时枚举所有思绪判断是否符合题意,时间复杂度 $O(nm)$。代码我没写。

Subtask.3

根据题面中的形式化题意,第 $j$ 天若第 $i$ 缕思绪存在,则其状态与 $(j-st_i)$ 对 $p=(x_i+y_i)$ 取模的值有关。同时由于 $st_i$ 是定值,所以本质上只与 $j$ 对 $p$ 取模的值有关。由于这个模数在本 Subtask 里很小,考虑开 $p^2$ 大小的数组存储答案。

设 $f_{p,i}$ 表示对 $p$ 取模值为 $i$ 的天数目前的答案。思绪出现时枚举 $[0,p-1]$ 作为 $i \bmod p$ 的结果,判断一下会对哪些余数造成贡献,存储在二维数组里,消失时把贡献撤销即可。查询只需要枚举 $p$,将所有 $f_p$ 的贡献求和,时间复杂度 $O(pm)$,空间复杂度 $O(p^2)$。可以参考 Subtask.4 后的代码。

最难做

发现每缕思绪本质上是向若干个区间贡献 $1$,所以考虑维护差分序列,在处理到第 $i$ 天时把前面的差分值加过来即可。但问题在于区间个数是 $O(\frac{m}{x_i+y_i})$ 级别的,可能会被卡到 $O(m^2)$,只能做 $(x_i+y_i)$ 较大的。

同时由于Subtask.3 中的做法只能做 $(x_i+y_i)$ 较小的,因此考虑按 $(x_i+y_i)$ 分治,超过 $B$ 的用差分做法,不超过 $B$ 的用 Subtask.3 中的做法,时间复杂度为 $O(mB+\frac{m^2}{B})$,分析一下可得 $B=\sqrt{m}$ 时最优,为 $O(m\sqrt{m})$。

考虑到 $O(mB)$ 全部跑满,而 $O(\frac{m^2}B)$ 完全跑不满,可以适当调低 $B$,但 std 仍使用了 $B=\sqrt m$。

n=read(),m=read(),B=sqrt(m\/2);
for(int i=1;i<=n;i++) x[i]=read(),y[i]=read();
for(int i=1;i<=m;i++)
{
	int opt=read(),k=read(),tr=0,mo=(x[k]+y[k]);
	if(mo<=B)
	{
		if(opt==1)
		{
			st[k]=i; 
			for(int j=0;j<mo;j++) if(((j-st[k])%mo+mo)%mo>=x[k]) f[mo][j]++;
		}
		else for(int j=0;j<mo;j++) if(((j-st[k])%mo+mo)%mo>=x[k]) f[mo][j]--;
	}
	else
	{
		if(opt==1) 
		{
			st[k]=i;
			for(int j=i;j+x[k]<=m;j+=mo)
			{
				s[j+x[k]]++;
				if(j+mo<=m) s[j+mo]--;
			}
		}
		else for(int j=(i-st[k])\/mo*mo+st[k];j+x[k]<=m;j+=mo)
		{
			if(j+x[k]>=i) s[j+x[k]]--;
			else s[i]--;
			if(j+mo<=m)
			{
				if(j+mo>=i) s[j+mo]++;
				else s[i]++;
			}
		}
	}
	for(int j=2;j<=B;j++) tr+=f[j][i%j];
	s[i]+=s[i-1],print(tr+s[i]),putchar('\n');
}

D.寄彩笺

Subtask.1

暴力枚举,分别枚举 $A,B$,对每个 $B$ 在每个 $A$ 中暴力查找个数。事实上这个点可以接受 $O(k^{n+m}nm)$ 的复杂度。代码我没写。

正解思路

考虑求每个 $B$ 在 $A$ 中贡献的次数,但也不能暴力枚举 $B$,所以要用某种东西给 $B$ 分类,使得同一类的 $B$ 对答案的贡献相同。

先考虑 $B$ 可重叠的贡献次数,这里长度 $len=m$,也就是在长为 $n$ 的 $A$ 中有 $(n-len+1)$ 种位置,钦定这些位置后其他位置随便填。所以长为 $len$ 的字符串可重叠的贡献次数为 $(n-len+1)k^{n-len}$。

接着考虑去掉重叠算多的贡献次数,发现前后两次会重叠当且仅当 $B$ 的一段前缀与 $B$ 的一段后缀相同,即若 $B=STS$,那么 $STSTS$ 出现时就会导致重叠。这里 $S,T$ 均为字符串,重叠部分 $S$ 即为 $B$ 的 border。

问题在于,若 border 长度超过 $m$ 的一半导致其本身有重叠,或 border 有多个,就无法使用唯一的 $STS$ 形式求解。然而,可以发现由于 border 完全相同,以上两种情况均会导致串内有三个或以上相同字符,而这在 $B$ 中是不会出现的,具体见下图:

所以可以保证字符串没有或仅有一个 border,有则不会自己重叠,那么可以用唯一 border 的长度作为开头说的分类方式。枚举 border 的长度 $x$,从 $k$ 个字母中选出 $x$ 个作为 border $S$。用剩余的 $(k-x)$ 个字母,每个使用不超过两次构成 border 中间的串 $T$,乘法原理乘起来就是这种 $B$ 的总数量。

之后容斥求答案,即加上 $STS$ 的贡献,减去 $STSTS$ 的贡献,再加上 $STSTSTS$ 的贡献,以此类推直到 $STSTS\cdots TS$ 长度超过 $n$ 时停止。当然这里的贡献均可重叠,这样能保证最终贡献的量为不重叠出现的次数。

这里要枚举 $ST$ 的重复次数 $i$,但由于对于长度 $x$ 只会枚举约 $\frac nx$ 个次数,且 $x$ 不会取满 $[1,n]$,所以复杂度不超过调和级数的 $O(n\ln n)$。注意特判无解,并单独处理一下没有 border 的情况即可。代码里的 $p$ 数组记录的是 $k$ 的若干次方。

if(k*2<m||n<m)
{
	cout<<0;
	return 0;
}
res=sol(k,m)*p[n-m]%mod*(n-m+1)%mod; int now=1;
for(int x=1;x*2<=m;x++) 
{
	now=now*(k-x+1)%mod;
	int ta=now*sol(k-x,m-2*x)%mod,w=1;
	for(int i=2; ;i++)
	{
		w*=-1;
		int len=m+(m-x)*(i-1);
		if(len>n) break;
		res=(res+ta*p[n-len]%mod*(n-len+1)%mod*w+mod)%mod;
	}
}
cout<<res;

但是还有一处没有解决,即 Sol 函数解决用 $tk$ 种字母,每种字母使用不超过两次,组成的长为 $tm$ 的字符串种类数,其中 $tk\in [k-\lfloor\frac m2\rfloor,k],tm\in[m\bmod2,m]$,以下给出解法:

Subtask.2.3

考虑 DP,设 $f_{i,j}$ 表示长为 $i$ 的字符串,其中 $j$ 个字母使用了 $2$ 次,剩余 $(i-2j)$ 个字母使用了 $1$ 次的方案数。转移只需要讨论第 $i$ 个字符是否使用过,从 $f_{i-1}$ 转移过来即可,答案为 $\sum f_{tm,j}$。

由于每次的 $tk$ 不相同,每次都要重新用 $O(m^2)$ 的时间复杂度 DP,$O(m)$ 次询问的总时间复杂度为 $O(m^3)$。(由于跑不满,搬题人在原赛时写这个把 $m\le2000$ 全卡过去了,所以这里最后开了 $m\le3000$)

int sol(int k,int m)
{
	f[0][0]=1; int res=0;
	for(int i=1;i<=m;i++) for(int j=0;j*2<=i;j++)
	{
		f[i][j]=f[i-1][j]*(k-(i-1-j))%mod;
		if((j-1)*2<=i-1) f[i][j]=(f[i][j]+f[i-1][j-1]*(i-1-2*(j-1))%mod)%mod;
	}
	for(int i=0;i*2<=m;i++) res=(res+f[m][i])%mod;
	return res;
}

最难做

枚举字符串中出现 $2$ 次的字符数量 $i$,考虑组合计算方案数,结果为 $\binom {tk}i \times \frac{A_{tm}^{2i}}{2^i}\times A_{tk-i}^{tm-2i}$,前两项选出 $i$ 个字符出现两次并安排好位置,最后一项对每个剩下的位置依次不重复地选一个剩下的字符。

由于 $tk\ge k-\lfloor\frac m2\rfloor$,考虑预处理 $k$ 的下降幂,即 ${fk}i=\prod{j=k-i+1}^k j$,也就是从 $k$ 开始向下前 $i$ 个数的积,预处理 $m$ 个即可。之后自己展开式子就行,要注意每次的 $tk$ 不同,要把 $[tk+1,k]$ 这一段的积约掉。

p[0]=p2[0]=fr[0]=fk[0]=1;
for(int i=1;i<=n;i++) p[i]=p[i-1]*k%mod,p2[i]=p2[i-1]*2%mod;
for(int i=1;i<=m;i++) fr[i]=fr[i-1]*i%mod;
for(int i=1;i<=min(m,k);i++) fk[i]=fk[i-1]*(k-i+1)%mod;
int sol(int tk,int tm)
{
	int res=0;
	for(int i=0;i*2<=tm;i++) if(tm-i<=tk)
	{
		int ts=1,dt=k-tk,dk=inv(fk[dt]);
		ts=ts*(fk[i+dt]*dk%mod)%mod*inv(fr[i])%mod;
		ts=ts*(fr[tm]*inv(fr[tm-i*2])%mod*inv(p2[i])%mod)%mod;
		ts=ts*(fk[tm-i+dt]*dk%mod*inv(fk[i+dt]*dk%mod)%mod)%mod;
		res=(res+ts)%mod;
	}
	return res;
}

F.重霄九

Subtask.1

每次询问直接对子树内每个点分别 $O(n)$ 判断是否合法,时间复杂度 $O(qn^2)$。代码我没写。

Subtask.2-4

考虑类似线段树维护每个结点子树内的最大值,最小值和子树内合法结点的数量,修改时修改点权后将其到根路径上的所有结点依次重新维护,时间复杂度 $O(qd)$,$d$ 为树的最大深度。由于数据随机时二叉树的期望深度为 $O(\log n)$,$O(q\log n)$ 的时间复杂度可以通过。

struct tree
{
	int lc[N],rc[N],a[N],s[N],mx[N],mn[N],fa[N];
	bool f[N];
	void pushup(int u)
	{
		int tl=lc[u],tr=rc[u];
		mx[u]=max(max(mx[tl],mx[tr]),a[u]),mn[u]=min(min(mn[tl],mn[tr]),a[u]);
		f[u]=(a[u]>=mx[tl]&&a[u]<=mn[tr]&&f[tl]&&f[tr]),s[u]=s[tl]+s[tr]+f[u];
	}
	void build(int u)
	{
		if(lc[u]) build(lc[u]);
		if(rc[u]) build(rc[u]);
		pushup(u);
	}
	void change(int u,int x)
	{
		a[u]=mx[u]=mn[u]=x;
		while(u) pushup(u),u=fa[u];
	}
}T;
signed main()
{
	n=read(),Q=read();
	for(int i=1;i<=n;i++) T.lc[i]=read(),T.rc[i]=read(),T.fa[T.lc[i]]=T.fa[T.rc[i]]=i;
	for(int i=1;i<=n;i++) T.a[i]=T.mx[i]=T.mn[i]=read();
	T.mx[0]=-inf,T.mn[0]=inf,T.s[0]=0,T.f[0]=1,T.build(1);
	while(Q--)
	{
		int opt=read(),x=read();
		if(opt==1) 
		{
			int y=read();
			T.change(x,y);
		}
		else print(T.s[x]),putchar('\n');
	}
	return 0; 
}

最难做

发现树的形态不变,那么若子树是二叉搜索树,其中最大值和最小值的位置是确定的,分别为其最靠右和最靠左的结点。这里最靠某方向的点是指从根节点一直走该方向的儿子,直到这个儿子为空时所停的点。

同时若点 $i$ 是二叉搜索树,那么其左右子树一定均为二叉搜索树,所以相对应的最值位置是固定的。可以预处理出每个结点左子树最靠右的结点 $lx_i$ 和右子树最靠左的结点 $rx_i$。

根据定义,这个点合法只需要其左右儿子均合法且 $a_{lx_i}\le a_i\le a_{rx_i}$。同时由于每个点不可能同时是两个点的 $lx$,$rx$ 也一样,加上其自身的限制,总共不超过 $3$ 条,修改时重新判断一遍即可。

所以只要一棵子树内所有点都满足自身的限制,这棵子树就是二叉搜索树。考虑设 $f_i$ 表示点 $i$ 的限制是否不被满足,即满足则为 $0$,不满足则为 $1$,那么子树内所有点都满足限制当且仅当子树内 $f_i$ 的和为 $0$。

考虑维护这个子树和 $c_i$,那么每个 $f_i$ 都会给其到根的路径上点的 $c_i$ 贡献,而答案为子树内 $c_i=0$ 的个数,也就是子树内最小值为 $0$ 时的最小值个数。所以用树剖 + 线段树实现路径加,同时维护子树最小值及个数即可,时间复杂度 $O(q\log^2n)$。

#include<iostream>
#include<vector> 
#define pii pair<int,int>
using namespace std; 
const int N=2e5+10;
int n,q,a[N],lc[N],rc[N],lx[N],rx[N],rm[N],lm[N],f[N];
void ini(int u)
{
	if(!u) return;
	ini(lc[u]),ini(rc[u]);
	rm[u]=rm[rc[u]],lm[u]=lm[lc[u]];\/\/lm 和 rm 分别表示最靠左和最靠右的结点 
	if(!rm[u]) rm[u]=u;
	if(!lm[u]) lm[u]=u;
	lx[u]=rm[lc[u]],rx[u]=lm[rc[u]];
}
vector <int> lim[N],edge[N]; 
int ct,siz[N],de[N],fa[N],son[N],id[N],top[N],aa[N],tw[N]; 
void dfs(int u,int fat)
{
	fa[u]=fat,de[u]=de[fat]+1,siz[u]=1,tw[u]=f[u];
	for(int v:edge[u])
	{
		dfs(v,u),siz[u]+=siz[v],tw[u]+=tw[v];
		if(siz[son[u]]<siz[v]) son[u]=v;
	}
} 
void dfsb(int u,int to)
{
	top[u]=to,id[u]=++ct,aa[ct]=tw[u];
	if(!son[u]) return;
	dfsb(son[u],to);
	for(int v:edge[u]) if(v!=son[u]) dfsb(v,v);
}
struct sgmtt
{
	int t=1,lc[N*2],rc[N*2],tag[N*2]; pii a[N*2]; \/\/最小值及个数 
	void pt(int u,int k) {a[u].first+=k,tag[u]+=k;}
	void pushdown(int u) {if(tag[u]) pt(lc[u],tag[u]),pt(rc[u],tag[u]),tag[u]=0;}
	void pushup(pii &u,pii tl,pii tr)
	{
		u.first=min(tl.first,tr.first),u.second=0;
		if(tl.first==u.first) u.second+=tl.second;
		if(tr.first==u.first) u.second+=tr.second;
	}
	void build(int u,int l,int r)
	{
		if(l==r) {a[u]={aa[l],1}; return;}
		int mid=(l+r)\/2;
		lc[u]=++t,build(t,l,mid);
		rc[u]=++t,build(t,mid+1,r);
		pushup(a[u],a[lc[u]],a[rc[u]]);
	}
	void add(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) add(lc[u],l,mid,ll,rr,k);
		if(rr>mid) add(rc[u],mid+1,r,ll,rr,k);
		pushup(a[u],a[lc[u]],a[rc[u]]);
	}
	pii check(int u,int l,int r,int ll,int rr)
	{
		if(l>=ll&&r<=rr) return a[u];
		int mid=(l+r)\/2; pushdown(u);
		if(rr<=mid) return check(lc[u],l,mid,ll,rr);
		if(ll>mid) return check(rc[u],mid+1,r,ll,rr);
		pii res;
		pushup(res,check(lc[u],l,mid,ll,rr),check(rc[u],mid+1,r,ll,rr));
		return res;
	}
}T;
void addrt(int x,int k)
{
	while(top[x]!=1) T.add(1,1,n,id[top[x]],id[x],k),x=fa[top[x]];
	T.add(1,1,n,1,id[x],k);
} 
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>lc[i]>>rc[i];
	ini(1),a[0]=-1,a[n+1]=n+1;
	for(int i=1;i<=n;i++) cin>>a[i],lim[i].push_back(i);
	for(int i=1;i<=n;i++)
	{
		if(lc[i]) edge[i].push_back(lc[i]);
		if(rc[i]) edge[i].push_back(rc[i]);
		if(lx[i]) lim[lx[i]].push_back(i);
		if(rx[i]) lim[rx[i]].push_back(i);
		else rx[i]=n+1;
		f[i]=!(a[lx[i]]<=a[i]&&a[rx[i]]>=a[i]);
	}
	dfs(1,0),dfsb(1,1),T.build(1,1,n);
	while(q--)
	{
		int opt,x; cin>>opt>>x;
		if(opt==2)
		{
			pii te=T.check(1,1,n,id[x],id[x]+siz[x]-1);
			cout<<(te.first?0:te.second)<<'\n';
		}
		else
		{
			int y; cin>>y;
			for(int i:lim[x]) addrt(i,-f[i]);
			a[x]=y;
			for(int i:lim[x]) f[i]=!(a[lx[i]]<=a[i]&&a[rx[i]]>=a[i]),addrt(i,f[i]);
		}
	}
	return 0;
}

Ex.凭阑意

以下设 $a_i$ 的值域为 $V$。

Subtask.1.2

发现答案上界为 $\max a_i$,因为 $p=\max a_i$ 时,最大值变为 $0$,其他值不变,一定不会有相同元素。所以从小到大枚举 $p$,$O(n)$ 用桶判重即可,总时间复杂度 $O(Vn)$。代码我没写。

Subtask.3

转换条件,发现 $a_i\equiv a_j\bmod p$ 等价于 $p\mid (a_i-a_j)$,也就是说若存在 $\left|a_i-a_j\right|$ 是 $p$ 的倍数,则 $p$ 不合法。所以可以 $O(n^2)$ 枚举差值,再从小到大找倍数中没有差值的数。由于枚举倍数的时间复杂度为 $O(V\ln V)$,总时间复杂度为 $O(n^2+V\ln V)$。

for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) if(a[j]>a[i]) f[a[j]-a[i]]=1;
for(int i=1;i<=V;i++)
{
	for(int j=i*2;j<=V;j+=i)
	{
		f[i]|=f[j];
		if(f[i]) break;
	} 
	if(!f[i]) {print(i);break;}
}

Subtask.4

同 Subtask.3 思路相同,从小到大判断数是否能作为 $p$,同样枚举倍数来判断,时间复杂度 $O(V\ln V)$。现在的问题在于如何快速判断 $a$ 中是否存在 $x$ 作为差值。

考虑使用 bitset 优化,开值域大小的 bitset $B$ 记录每个值是否存在,那么存在 $x$ 作为差值当且仅当 $B$ 左移或右移 $x$ 位后按位与 $B$ 的结果中有 $1$。

再开个数组记忆化,保证每个 $x$ 只 check 一遍,时间复杂度 $O(\frac{V^2}w)$,总时间复杂度 $O(V\ln V+\frac{V^2}w)$。这也是最后一个 Subtask 值域缩小到 $10^5$ 的原因,得到满分需要把后两个 Subtask 拼起来

bitset <M> bs;
bool vis[N],ma[N];
bool check(int x)
{
	if(!vis[x])
	{
		vis[x]=1;
		if(((bs>>x)&bs).count()) ma[x]=1;
		else ma[x]=0;
	}
	return ma[x];
}
void solb()
{
	for(int i=1;i<=n;i++) bs[a[i]]=1;
	for(int i=2;i<=V\/10;i++)\/\/ V\/10 即为本 Subtask 的值域 10^5 
	{
		bool tf=true;
		for(int j=i;j<=V\/10;j+=i) if(check(j)) {tf=false; break;}
		if(tf) {print(i); break;}
	}
}

P10053 题解

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

思路

考虑树形 DP,提前处理出每个结点处释放的球数 $a_i$,以每个点为根的子树大小 $siz_i$,以每个点为根的子树内释放的球数 $cnt_i$。以下记 $K$ 为总球数,$b_i$ 表示 $i$ 子树内最多能容纳的外部球数,$b_i=siz_i-cnt_i$。

设 $f_{i,j}$ 表示以 $i$ 为根的子树内由其父亲落入了 $j$ 个球的方案数,有意义的 $j$ 满足 $j\le K-cnt_i$ 且 $j\le b_i$。这里还需要详细定义以避免算重,也就是说每个点只被确定一次顺序。

因此我们在处理 $f_i$ 时确定在 $i$ 处释放的球的先后顺序,可以避免算重。所以 $f_{i,j}$ 的定义变为只考虑 $j$ 个最终位置点集的方案数,也可以说暂时认为这些球是相同的,无先后顺序。这样做的本质是把所有球按释放位置分组,先确定每一组所占的位置集合,然后再在组内排列确定顺序,不会计重。

考虑转移,首先发现这 $j$ 个球在落入 $i$ 时一定已经确定之后优先走的子树,这取决于 $i$ 是其父亲的哪个儿子,需要记录,在 DFS 里用参数记下来即可。我们此处设 $to$ 为优先走的子树根,$ano$ 为另一个子树根,没有则为 $0$。

另外还要确定的就是在 $i$ 处释放的 $a_i$ 个球的方向,考虑枚举其落入 $to$ 子树的球数 $k$,有意义的 $k$ 满足 $k\le b_{to}$ 且 $k\le a_i$,剩余的 $(a_i-k)$ 个球落入 $ano$ 子树。

那么从父亲来的 $j$ 个球就可能会由于 $to$ 子树被填满而去 $ano$ 子树,会剩下 $y=\min(j,b_{to}-k)$ 个球真正落入了 $to$ 子树,另外 $(j-y)$ 个落入 $ano$ 子树。

由于从父亲来的 $j$ 个球没有区别,选择 $y$ 个的方案数为 $1$。所以这里的方案数由以下几部分组成:

  • 从 $a_i$ 个中选出 $k$ 个落入 $to$ 子树。
  • 从落入 $to$ 子树的共 $(k+y)$ 个球中选出 $k$ 个作为从 $i$ 释放的,并确定顺序。
  • 从落入 $ano$ 子树的共 $(a_i-k+j-y)$ 个球中选出 $(a_i-k)$ 个作为从 $i$ 释放的,并确定顺序。
  • 两个子树内部的方案数。

所以对于给定 $(j,k)$ 的方案数为:$\binom{a_i}{k}\times A_{k+y}^k\times A_{a_i-k+j-y}^{a_i-k}\times f_{to,k+y}\times f_{ano,a_i-k+j-y-flag}$,其中 $A$ 为排列数, $flag=[j=b_i]$,即如果 $j$ 个球落入后把 $i$ 子树填满了,则最后一个球会留在 $i$ 点,不会落入 $ano$ 子树,需要减去。

最终把所有 $k$ 算出的方案数累加起来,便为 $f_{i,j}$ 的最终值,最终答案为 $f_{1,0}$。另有很多边界问题,这里只需要保证所有无法达到的 $(i,j)$ 满足 $f_{i,j}=0$ 即可,因此初值为空树的 $f_{0,0}=1$,其余为 $0$。

代码

#include<iostream>
#define int long long
using namespace std;
const int N=4e3+10,mod=1e9+7;
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 fr[N],ifr[N];
int C(int n,int m) {return fr[n]*ifr[m]%mod*ifr[n-m]%mod;}
int A(int n,int m) {return fr[n]*ifr[n-m]%mod;}
int temp,n,kk,a[N],b[N],l[N],r[N],siz[N],cnt[N],f[N][N];
void dfs(int i,bool lr)
{
	int lc=l[i],rc=r[i];
	if(lc) dfs(lc,1);
	if(rc) dfs(rc,0);
	siz[i]=siz[lc]+siz[rc]+1,cnt[i]=cnt[lc]+cnt[rc]+a[i],b[i]=siz[i]-cnt[i];
	for(int j=0;j<=b[i]&&j<=kk-cnt[i];j++)
	{
		int to=(lr?lc:rc),ano=(lr?rc:lc);
		for(int k=0;k<=b[to]&&k<=a[i];k++) 
		{
			int y=min(j,b[to]-k),flag=(j==b[i]);
			int ta=f[to][k+y]*A(k+y,k)%mod,tb=f[ano][a[i]-k+j-y-flag]*A(a[i]-k+j-y,a[i]-k)%mod;
			f[i][j]=(f[i][j]+C(a[i],k)*ta%mod*tb%mod)%mod;
		}
	}
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>kk,fr[0]=ifr[0]=f[0][0]=1;
	for(int i=1;i<=n;i++) fr[i]=fr[i-1]*i%mod,ifr[i]=po(fr[i],mod-2);
	for(int i=1;i<=kk;i++) cin>>temp,a[temp]++;
	for(int i=1;i<=n;i++) cin>>l[i]>>r[i];
	dfs(1,0),cout<<f[1][0];
	return 0; 
}

CF196E 题解

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

思路

复杂度中认为 $n,m,k$ 同级。

发现打开第一个传送门后,打开新传送门的过程类似于 Prim 求最小生成树,也就是找到已打开点集和未打开点集之间的最短路径,然后直接传送到已打开的 $S$ 点,再走到未打开的 $T$ 点并打开。

显然中间走的路径是最短路,所以对 $k$ 个点建完全图,边权为两个点在原图上的最短路径。之后在 $k$ 个点的新图上跑最小生成树,答案即为最小生成树的权值和加上起点到传送门的最短距离。时间复杂度瓶颈在预处理两点之间最短路径,用 Dijkstra 的话大概是 $O(n^2\log n)$。

考虑减少新图中的边数,设 $p_i$ 表示离 $i$ 最近的传送门编号,$dis_i$ 为距传送门的距离。那么对于一条权值为 $w$ 的边 $(u,v)$,在新图上建边 $p_u\to p_v$,边权为 $dis_u+dis_v+w$,这样就能包含所有最终最小生成树中的边。

笔者太菜了,完全想不出这个方法,读了官方题解之后自己证明如下:

以下设 $d_{i,j}$ 表示点 $i$ 和点 $j$ 之间的最短路径长度,两个集合即为上述已开启和未开启的点集。

对于最终在最小生成树中的一条边(原图中的路径) $S\to T$,若路径前半部分的 $p_i=S$,后半部分的 $p_i=T$,那么路径上一定存在一条边 $(u,v)$ 满足 $p_u=S$ 且 $p_v=T$。现在只需要证明路径上 $p$ 的情况如此即可。

使用反证法,假设 $S\to u$ 的路径上有点 $x$ 使得 $p_x\ne S$,设 $p_x=E$,那么 $x\to u$ 的路径上点的 $p$ 均为 $E$。

若 $E$ 与 $S$ 属于同一集合,与 $T$ 属于不同集合,那么由于 $d_{E,u}\le d_{S,u}$,可以取 $E \to T$ 这条路径。

否则 $E$ 与 $S$ 属于不同集合,由于路径的包含和 $p$ 数组的定义,有 $d_{E,x}\le d_{E,u}\le d_{S,u}\le d_{u,T}\le d_{x,T}$,最终得到 $d_{E,x}\le d_{T,x}$,可以取 $S\to E$ 这条路径。

$v\to T$ 路径上的情况相同。式子比较抽象,可以结合下面丑陋的图。

所以最终只需要以 $k$ 个点为起点用 Dijkstra 跑单源最短路,预处理出 $p,dis$ 数组即可,时间复杂度降为 $O(n\log n)$。

代码

#include<iostream>
#include<vector> 
#include<queue>
#include<algorithm>
#define pii pair<int,int>
#define int long long
using namespace std; 
const int N=1e5+10;
const int inf=2e18;
int n,m,k,res,p[N],to[N],dis[N],tu[N],tv[N],tw[N],f[N];
bool vis[N];
vector <pii> edge[N];
struct nod {int u,v,w;}ne[N];
bool cmp(nod ta,nod tb) {return ta.w<tb.w;}
priority_queue <pii> q;
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;
	for(int i=1,u,v,w;i<=m;i++)
	{
		cin>>u>>v>>w,tu[i]=u,tv[i]=v,tw[i]=w;
		edge[u].push_back({v,w}),edge[v].push_back({u,w});
	}
	for(int i=1;i<=n;i++) dis[i]=inf,to[i]=-1;
	cin>>k;
	for(int i=1;i<=k;i++) cin>>p[i],to[p[i]]=i,dis[p[i]]=0,q.push({0,p[i]});
	while(q.size())
	{
		int u=q.top().second; q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(pii te:edge[u])
		{
			int v=te.first,w=te.second;
			if(dis[v]>dis[u]+w) dis[v]=dis[u]+w,to[v]=to[u],q.push({-dis[v],v});
		}
	} \/\/Dijkstra 预处理 
	for(int i=1;i<=m;i++) ne[i]={to[tu[i]],to[tv[i]],dis[tu[i]]+dis[tv[i]]+tw[i]};
	sort(ne+1,ne+1+m,cmp);
	for(int i=1;i<=k;i++) f[i]=i;
	for(int i=1;i<=m;i++)
	{
		int fu=finds(ne[i].u),fv=finds(ne[i].v),w=ne[i].w;
		if(fu!=fv) f[fu]=fv,res+=w;
	} \/\/建新图 Kruskal 跑最小生成树 
	cout<<res+dis[1];
	return 0;
}

【比赛记录】ABC359

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-06-22 23:17:56

记录

AB 速切,C 简单分讨但是调了 20min,DEF 也都正常做,没吃罚时,还行。

题解

A - Count Takahashi

基础判断,略过不表。

B - Couples

记录 $l_i,r_i$ 分别为 $i$ 两次出现的位置,$r_i-l_i=2$ 的 $i$ 的数量即为答案。

C - Tile Distance 2

首先 $y$ 方向的花费一定为 $\left|T_y-S_y\right|$。考虑横向的花费,这里先通过交换保证 $S_x\ge T_x$。然后考虑走到 $T_y$ 行时最多能走到哪一列,首先要到 $(S_x,S_y)$ 所在区域的最左边,然后每走一行可以向右移一格,最后到达目标行后每走两格需要 $1$ 代价,分讨一下即可。

D - Avoid K Palindrome

状压 + 计数 DP。发现 $k$ 特别小,以至于 $2^kn$ 只有 ${10}^6$ 级别,考虑把最近 $k$ 位的情况设入状态,设 $f_{i,j}$ 表示填了前 $i$ 位,且最近的 $k$ 位状态为 $j$ 的方案数,这里用 $0$ 表示 A,用 $1$ 表示 B。

由于要求只有长度为 $k$ 的子串皆不回文,所以边界可以为 $f_k$,方便处理。每次枚举第 $i$ 位填 $0$ 或 $1$,从 $f_{i-1,j}$ 转移时判断 $j$ 右移后加上该位的状态是否合法,即不回文且符合原串要求,若合法就累加即可。

E - Water Tank

发现任意时刻有效的挡板都是高度递减的,也即若 $i<j$ 且 $a_i\le a_j$,那么不考虑 $a_i$ 是不会影响答案的,$i$ 的上一块和 $j$ 之间的高度一定为 $a_i$。

所以用一个栈维护目前的有效挡板,放入二元组 $(h,k)$,表示高度为 $h$ 的目前有 $k$ 块。从 $1$ 到 $n$ 依次处理时,每次都直接压入 $(a_i,1)$,然后处理栈顶直到元素只剩一个或最后两个递减,也就是维护一个单调栈。每次弹出元素时把差值的贡献补上即可。

F - Tree Degree Optimization

显然排序以后不会影响最终答案的值,直接按 $a_i$ 不降排序。考虑如果构造 $f_i<i$ 表示 $i$ 向 $f_i$ 连边,会比较方便处理。

证明比较容易,因为如果一种方案存在 $f_k>k$,那么可以把这两个点交换。具体地,若 $d_{f_k}>d_k$ 直接交换,否则在交换时通过子树移动保持两者的度数不变,保证了方案不劣。同时 $d^2a$ 中 $d$ 增加 $1$ 时,变为 $(d^2+2d+1)a$,产生的代价为 $(2d+1)a$。

所以初始设 $d_i(i\in[2,n])=1,d_1=0$,开小根堆存储所有可能产生的代价及编号,初始压入 $(1,a_1)$。之后每次使用 $k$ 时都把 $d_k$ 加上 $1$,之后压入 $(2d_k+1)a_k$,全部处理完之后得到的即为最小代价。

G - Sum of Tree Distance

考虑把贡献拆到边上,$x$ 连向父亲的边贡献为 $\sum_{i=1}^n s_{x,i}\times (s_{1,i}-s_{x,i})$,合并时使用启发式合并,均摊复杂度 $O(n\log n)$。

具体操作是用 map 维护子树内所有的颜色种类和个数,另外设 $v$ 为目前点连向父亲的边的贡献,初始为 $s_{1,c_x}-1$,也就是叶子节点的贡献。合并时先减去父亲节点子树原有的贡献,然后把子节点子树的数量并上,再用式子计算整个父亲子树与外界的贡献即可。

【比赛记录】CFR954(Div.3)

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

记录

A-F 切掉,但是细节都很多,调的时间比较长。G 不会。

题解

A. X Axis

枚举 + 循环判断即可,略过不表。

B. Matrix Stabilization

把所有需要操作的格子找出来,按照要求排序,依次处理直到为四周的最大值,显然不会有格子被重复操作,复杂度正确。

C. Update Queries

显然操作序列都可以排序。把操作下标用桶存一下,操作字符升序排序,顺序处理,每到一个可以操作的下标 $i$ 就使用剩余的最小值修改,若需要对其操作多次可以看作用最大的若干个修改过后用最小值覆盖掉,所以这样一定是合法且最优的。

D. Mathematical Problem

枚举每一个位置作为空出来的位置,然后可以得到剩余数的序列 $b$。考虑如何操作让结果尽量小,发现若有 $0$ 一定是 $0$,其他的 $1$ 全乘进去,而大于 $1$ 的直接加起来一定最优,因为已经不考虑 $1$,剩余的两个数 $a+b$ 一定不大于 $ab$,时间复杂度 $O(Tn^2)$

E. Beautiful Array

发现两个数 $x,y$ 要变为相等必须保证两数模 $k$ 相等,需要消耗的操作次数为 $\left|\frac xk-\frac yk\right|$。所以按照对 $k$ 取模的余数分组。

若 $n$ 为偶数则必须每组都有偶数个,在每组里每相邻两个放到对应位置即为代价最小。若 $n$ 为奇数则只能有一组个数为奇数,其他组里同样计算代价,在奇数组里求出前缀和后缀每两个组合的代价和,枚举把哪一个放在正中间,取最小值即可。

F. Non-academic Problem

发现若去掉的边不是割边,去掉后答案仍为 $\frac{n(n-1)}2$,否则就把整个图分成了两部分,每一部分单独为 $\frac{k(k-1)}2$。只要 Tarjan 跑出割边然后树上求出子树大小,枚举断掉每个点连向父亲的边即可。

G. Permutation Problem

整个题目的复杂度都依赖于 $O(n\ln n)$ 调和级数枚举倍数。

题意即为找 $i,j$ 的数量使得 $\frac{p_ip_j}{ij}$ 为整数。考虑去掉本身的影响,设 $g=\gcd(p_i,i)$,记录 $a_i=\frac{p_i}{g},b_i=\frac{i}{g}$,所以就变成了 $b_j \mid a_i$ 且 $b_i\mid a_j$ 的数量。

过程即为枚举每个 $B$,处理所有 $b_i=B$ 的 $i$。再枚举 $B$ 所有的有效倍数 $A$,记录下所有 $a_j=A$ 的 $b_j$ 出现次数,再回来用所有 $b_i=B$ 的 $a_i$ 查找其因数在前面的 $b_j$ 中出现的次数,就是满足条件的方案数。

实现上必须每次只对有效元素进行操作和清空,不能在值域上进行,否则复杂度不对。同时还要减去自己与自己搭配的方案数,也就是 $i \mid p_i$ 的个数,再除掉每组重复的 $2$ 次即为答案。

【比赛记录】周测 #14

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-06-28 22:53:23

记录

赛时 A 假 B 不会,C 调过了,D 也假,E 没看,比较寄。

题解

A. P7667 [JOI2018] Art Exhibition

发现由于要价值和减去尺寸极差最大,且价值均为正,所以若按尺寸排序,最优解一定是一段连续区间。

考虑预处理价值的前缀和 $s$,设取的区间左右端点为 $x,y$。拆式子,得到 $S-(A_{max}-A{min})=s_y-s_{x-1}-(A_y-A_x)=(s_y-A_y)+(A_x-s_{x-1})$,枚举其中一个括号,另一个维护前后缀最大值即可。

B. P7668 [JOI2018] Dango Maker

如果均不冲突,暴力求个数即可。需要考虑的是有冲突时如何计算,通过举例发现任意两个冲突的串一定是一横一竖,同时 G 在同一条左下-右上对角线上相邻或相同。

所以对于每一条对角线分别考虑,由于两条对角线之间没有相互影响,相互独立。这时设 $f_{i,0/1/2}$ 表示前 $i$ 个中第 $i$ 个不选/竖选/横选的代最大个数,只要保证上一个不选或上一个与当前的串方向不同即可。

C. P6304 [eJOI2018] 山

考虑设 $f_{i,j}$ 表示前 $i$ 座山上选 $j$ 个的最小代价,发现难以转移,所以加一维 $k=0/1$ 表示第 $i$ 个本身是否被选,同时若 $i$ 被选保证 $a_{i-1}<a_i$ 的最小代价,这样能防止重复计算代价。

所以 $f_{i,0}$ 从 $f_{i-1}$ 直接转过来,若选 $i-1$ 还要保证把 $a_i$ 减至小于 $a_{i-1}$。$f_{i,1}$ 从 $f_{i-2}$,转过来,要把 $a_{i-1}$ 减至小于 $a_i$,若同时也选 $i-2$ 也要保证小于 $a_{i-2}$。

D. P5089 [eJOI2018] 元素周期表

考虑建模成一个二分图,左部点表示行,右部点表示列,每两个点之间的连接情况相当于原图中的一个格是否被填。发现最终要求转化为完全图,同时每个连通块内一定能转化成局部完全图。所以把原有的点作为边,答案等于连通块个数 $-1$。

E. P7669 [JOI2018] Commuter Pass

发现最终答案一定是一段非 $0$,一段 $0$ 再一段非 $0$,军可能为空。因为如果有多段 $0$ 却不连续,那么前一段的结尾 $X$ 和后一段的开头 $Y$ 一定都在 $S$ 到 $T$ 的最短路上,所以一定可以用中间的 $0$ 补起来,一定较优。

但是暴力枚举 $0$ 的两端 $A,B$ 会产生 $O(n^2)$ 的时间复杂度,寄。考虑优化,把 $S$ 到 $T$ 的最短路树建出来,一定是以 $S$ 为源点的 DAG。在上面跑拓扑排序以求出 $U$ 到每个点的最短距离,初始化为原来的最短距离,对于每个 $v$ 更新 $dis_v=\min(dis_v,dis_u)$,也就是走了一条 $0$。最后用 $dis_i+dis_{i,V}$ 更新即可。由于可能以最短路的反向经过最短路,还要建反图再跑一遍。

【比赛记录】EDU167

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-06-30 17:44:41

记录

A 题唐了吃一发,后边还行,D 题想了一会最终也写出来了,E 感觉很困难没做出。

本来忘了写记录了,现在补一下。

题解

A. Catch the Coin

首先必须要走至少 $\left|x\right|$ 步以到达同一列,同时如果能在某一时刻走到金币正下方,就一定能通过操作来抓住掉下来的金币。所以一直向斜下方走到金币那一列,只要能与金币在同一位置或在正下方即能拿到。

B. Substring and Subsequence

枚举把整个 $a$ 放在 $b$ 的哪个位置之后,然后贪心地尽可能多地匹配上子序列即可。

C. Two Movies

发现若某人对两部电影评价不同,则让其选择评价高的一者一定不劣。否则记录下对两部电影都好评/差评的次数,贪心地尽可能减小两部电影的分数差即可。

D. Smithing Skill

发现金属充足时,一定会先选择损耗最小的一种来获取经验直到不足以制造武器,这样就可以保证剩余的金属不超过 $10^6$。

接下来要考虑统一处理所有的金属量,使得最终可以实现 $O(1)$ 查询。设 $f_i$ 表示剩余 $i$ 金属时最多可以制造的武器数,再设 $s_i$ 表示需要金属不超过 $i$ 的方案中损耗金属的最小值,则 $f_i=\max(f_{i-s_i}+1,f_{i-1})$。预处理好之后分别对 $m$ 个查询答案即可。

E. Distance to Different

发现询问的方案数等价于把整个长度为 $n$ 的序列分成 $x(x\ge k)$ 段的方案数,所以设 $f_{i,j}$ 表示把 $i$ 个元素分成 $j$ 段的方案数,由于段数多后没有特殊限制,所以可以把 $x>k$ 的方案数全部压到 $j=k+1$ 里,得到 $f_{i,j}=\sum_{i'=j-1}^{i-1}f_{i',j-1}$,前缀和处理一下复杂度就对了。

还要注意若存在一段序列中间长度为 $2$ 的段,那么 $b_i=b_{i+1}=1$,与分成两段 $1$ 的结果相同,所以要把这种情况减去。当然若在序列两段则可以取到 $2$,因为这样会使得端点的值为 $2$。

【听课记录】24.07-HB

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

24.06.30

  • 用枚举替代分讨(CF105C)。
  • 寻找合适的模拟方式(Gym100211C)。
  • ARC 题目难度适中,相当于联赛难度。
  • 题目难度更多在思维,板子需要多训练(ARC180D)
  • DP 状态的设计和复杂度分析(ARC180C)

07.01

  • 要想好全部思路再开始写代码(Gym100644H)
  • 对一个题要多方面地思考,用多种解法,同时思考题目背后的思维难度和强化版本(P9753.CF1223F)

07.02

  • 边界等细节很重要,要在细节处提升(ABC360F.G)
  • 模板要学会应用(CF1987F)

07.03

  • 考虑去掉无用元素或状态(CF76A.C)
  • 循环代替分讨来简化代码(CF76B)
  • 处理好边界,想好再写码,少点分讨不容易错(P9871)

07.04

  • 写码习惯上有不开 define int long long 等(我就这样);DP 时可以多开一个 $g$ 数组辅助,减少分讨。(P9871)
  • 搜索题可以分步搜,对等价内容只搜一次,减少复杂度(Gym100825C)

07.05

  • 搜索时从搜的内容入手,相似等价的内容只搜一次,降低复杂度(CF293B)
  • 搜索中通过状压剪枝是很重要的(Gym100291A)

07.06

  • 带记忆化的搜索有时可以用 dp;可以考虑把一部分拿出来单独计算以减少状压位数(CF293B)
  • 题目一定要注意数据范围,有时极小的数据范围可以接受若干个依次操作(CF264E)

07.07

  • 从宏观到微观,有时可以把大规模的问题通过充要条件转化为小问题分别处理(CF525D)

07.08

  • 要看清问题的本质,精准找到正确的路,而不是靠运气做对(Gym105244C)

07.09

  • 读题很重要.(SGU147.148.167)

07.10

  • 可以把抽象的问题建模为直线上的区间问题(SGU349)
  • 多种数据结构都得会(SGU311)

07.12

  • DP 最重要的是看出题目的特殊性质并进一步设出状态,而数据结构优化的部分往往没有思维难度(SGU485)

07.13

  • 抓住题目中的特殊条件,以此简化问题(CF76C)

07.14

  • 运用数学有关知识证明求解问题。

07.15

  • 通过转化把问题变为经典模板进行求解(ABC-EF)

07.16

  • 数学有关内容要理解其本质和推导过程,才能熟练运用(ABC137F)

07.17

  • 没啥好说的好像

07.18

  • 三天不学习,比不过
  • 非必要不看题解 / 买书 / 学新知识点
  • ABC 的后几题比较适合作为 NOIP 前三题

07.19

  • 要做会题,不能见到原题都不会
  • 要看清楚题目之间的联系,更要看清联系之间的联系(CF1994F

07.20

  • 没啥好说的好像

07.21

  • 数学要从会的部分扩展出去
  • 尺规作图蕴含了很多
  • 古巴比伦发现了很多
  • 定理需要证明,而且或许要用简单的东西证明,比如带余除法
  • 要理解定理的证明和解法,熟练运用

07.22

  • 接着讲数学和群论,没啥好说的好像

【补题记录】ARC180

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-07-01 22:42:04

记录

赛时在车上还很困,A 都没做出来直接跑了。CD 老师讲了,写一下记录。

题解

A - ABA and BAB

发现对于每个长度为奇数且相邻两个字母不相同的子串,都可以删除其中任意多个 AB(或者说是 BA),也就是长为 $k$ 的子串可以有 $\frac{k+1}2$ 种结果。所以对于每个这样的极长子串记录方案数,乘法原理乘起来即可。

这样不会算重是因为两段之间一定是互不影响的,而且相邻两个极长段之间要么字符相同,要么是极长段长为偶数多出来的,都会有字符分隔,所以乘法原理正确。

C - Subsequence and Prefix Sum

发现会影响接下来操作的只有目前的前缀和,而不关心这个前缀和是怎么来的。又因为值域小,总和的绝对值不超过 $1000$,可以设入状态,设 $f_{i,j}$ 表示前 $i$ 位中目前前缀和为 $j$ 的方案数。

所以不选 $i$ 时有 $f_{i,j}=f_{i,j}+f_{i-1,j}$,否则有 $f_{i,j+a_i}=f_{i,j+a_i}+f_{i-1,j}(j\ne0)$。这里 $j$ 不等于 $0$ 是因为如果用 $0$ 来转移,那么 $a_i$ 不会有变化,会导致计重。

所以设 $g_i$ 表示上一次为 $0$ 又加上了 $i$ 的方案数,每次令 $g_{a_i}=g_0,g_0=f_{i,0}$ 即可。前面的转移也会变成 $f_{i,j+a_i}=f_{i,j+a_i}+f_{i-1,j}+g_j(j\ne0)$,这样就没问题了。

D - Division into 3

先考虑朴素做法,发现区间内的最大值一定会贡献到答案里。那么分这个最大值 $p$ 在哪一段讨论。若在第二段,那么左右各取最边上的一个一定最优。若在两边,那么中间的第二段只取一个一定不劣,这里假设取左边的第一段,那么就要求 $i\in[p+1,r-1]$ 使得 $a_i+\max_{k=i+1}^r a_k$ 最小。

考虑把所有这样的询问 $[p,r]$ 离线下来,然后对整个长度为 $n$ 的区间操作。发现 $a_i$ 只会影响到 $[k,i-1]$ 这段区间,其中 $a_k$ 是 $i$ 前面最近的大于 $i$ 的数。那么所有最终有影响的 $a_i$ 一定单调递减,可以用个单调栈维护。

所以设 $f_i$ 表示处理到目前的 $r$ 时的 $a_i+\max_{k=i+1}^r a_k$,如果能在依次处理每一个元素的同时维护这个数组,那只要在处理完了询问的 $r$ 后求 $[p+1,r-1]$ 区间的 $f_i$ 最小值即可。那么每处理到 $i$ 就给 $f_{i-1}$ 赋值 $a_{i-1}+a_i$,然后维护单调栈,弹出元素 $x$ 时意味着 $x$ 的上一个元素直到 $x-1$ 这一段区间 $f_i$ 中的最大值不能取 $a_x$ 而是要取 $a_i$ 了,所以这段要加上 $a_i-a_x$。

综上,要支持的操作有:

  • 单点修改
  • 区间加
  • 区间求最小值

所以线段树就行了。

【比赛记录】CF1987(Div.1+2)

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-07-01 22:43:49

记录

切了 ABCE,D 对着贪心调俩小时过不了,后来发现是 dp。老师要讲 FG,感觉 F 还行吧。

题解

A. Upload More RAM

答案显然为 $k(n-1)+1$,略过不表。

B. K-Sort

要求最终序列单调不降,考虑最终每个数至少需要加多少,也就是 $b_i=\max_{j=1}^{i-1}a_j-a_i$,维护目前的最大值即可求出。然后把大于 $0$ 的 $m$ 个 $b_i$ 拿出来升序排序。

发现每次操作都给所有还需要加的加上,这样操作比较集中,一定不劣。所以给后 $i$ 个一起加需要 $s_i-s_{i-1}$ 次,代价为 $m-i+2$,把所有的 $(s_i-s_{i-1})(m-i+2)$ 求和即为答案。

C. Basil's Garden

设 $f_i$ 表示 $a_i$ 变为 $0$ 需要的轮数,分讨一下 $a_i$ 和 $a_{i+1}$ 的大小关系,有边界 $f_n=a_n$,转移 $f_i=\max(f_{i+1}+1,a_i)$,对 $f_i$ 取最大值即可。

D. World is Mine

发现美味值相同的蛋糕可以分为同一组,可以转化为若干个数量 $b_m$。显然 Alice 只要选目前可选的美味值最低的就好了,因为这样后续可操作次数一定不劣。而 Bob 要考虑的事情就比较多了,需要把某一美味值的所有蛋糕在 Alice 选到之前全都选掉才能阻止 Alice 选。

所以转化为要在某一次数限制内消耗 $c_i$ 次来使 Alice 少选到一种,考虑反悔贪心,设 $cur$ 为目前剩余的次数,$res$ 为 Alice 选到的数量,再开个大根堆记录已经选过的 $c_i$。初始 $cur=0,res=0$,每次若 $c_i\le cur$ 就直接扔堆里,否则本次一定空了,$cur,res$ 均加上 $1$。同时若堆顶比 $c_i$ 大,可以用 $c_i$ 替换来省出一些次数加入 $cur$。最终 $res$ 就是答案。

E. Wonderful Tree!

叶子节点本身就合法,考虑从叶子节点向上 dp,也就是保证所有子树已经全部合法的情况下合并到根。初步理解是如果要让节点 $u$ 的子节点权值和增大,也就是给某个子节点加权值时,需要一直传递到某一叶子节点以保持子树的合法性。

但是写出来挂了,因为如果某一节点 $u$ 本来就合法,也就是 $a_u\ge \sum a_v$,那么这个点可以直接加权值而不用继续往下传递。所以设 $f_{u,i}$ 表示距离 $u$ 为 $i$ 的节点还能直接加多少权值。

则对于所有叶子节点 $x$ 均有 $f_{x,0}=+\infty$,合并到根时有 $f_{u,i}=\sum f_{v,i-1},f_{u,0}=a_u-\sum a_v$。同时每到一个节点使用剩余容量使其合法即可,可以发现这样在处理完整颗子树后根节点的权值不变,所以是正确的。

F. Interesting Problem

发现能用 $a_i$ 消除必须保证下标 $x=a_i$,也就是说需要在其前面删掉恰好 $b_i=i-a_i$ 个元素,所以 $i\ge a_i$ 且 $i$ 与 $a_i$ 同奇偶才有可能消除。然后考虑设 $f_{l,r}$ 为删掉 $[l,r]$ 需要在 $[1,l-1]$ 中操作的最少次数,然后区间 dp 枚举断点,还要考虑 $l$ 和 $r$ 为一组最后消除的情况。因此有:

  • $f_{l,r}=\min(f_{l,r},f_{l+1,r-1}),f_{l+1,r-1}\le b_l$
  • $f_{l,r}=\min_{k=l+1}^{r-2}\max(b_l,f_{l,k},f_{k+1,r}-\frac{k-l+1}2)$

然后再进行另一个 dp ,设 $g_i$ 表示前 $i$ 个数中的最大操作次数,当然可以转移 $g_i=g_{i-1}$。然后枚举上一轮断点 $j$,即 $g_i=\max_{j=0}^{i-1}g_j\ge f_{j+1,i}$,最后取 $g_n$ 即为答案。

共 137 篇博客