12月の题

2023快结束了

CF1000G Two-Paths

本来还以为挺简单的,结果越想越不对,然后从昨天晚上写到今天早上。稍微观察一下就珂以发现这是个换根,考虑维护一些值:\(f_u\) 表示以 \(\mathbf{\small{1}}\) 为根时 \(u\) 子树内可得的最大贡献,\(g_u\) 表示以 \(\mathbf{\small{1}}\) 为根时 \(u\) 的父亲的子树内不走 \(u\) 的子树时的最大贡献,\(dp_u\) 表示以 \(u\) 为起点可获得的最大贡献(珂以不走子树),这些珂以换根预处理出来,并且我们都默认每条边走两遍,注意 \(f_u,g_u\) 要从叶子向上处理。
然后查询的时候考虑分情况讨论:当 \(u,v\) 在同一条链上时我们假设 \(u\) 是深度较大的点,贡献由 \(u\) 的子树、除 \(v\) 子树的部分和路径上的贡献组成。令 \(u'\)\(v\)\(u\) 的方向走一步到达的那个点,我们要减去 \(g_{u'}\)\(dp_v\) 中的贡献,然后加上 \(u\to u'\) 路径上的点的 \(g\) 值,加上路径上点的权值(没算到)减去这条路径的边权和(只走一遍不乘二),还要加上 \(f_u\) 的贡献;
\(u,v\) 不在一条链上时我们向上面一样令相同定义的 \(u',v'\),两条链的贡献分别计算,并且注意 \(lca\) 的贡献是否被重复算过;如果 \(u=v\) 贡献就是 \(dp_u\)。至于找 \(u',v'\) 珂以通过先倍增找到链的长度(不算边权)然后将长度减一去找到那个点,然后这个题就做完了,细节贼多。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3*114514,M=1919810;
struct xx{
	ll next,to,val;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
}
ll ff[N][32],dept[N],lg[N];
void dfs_pre(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		ff[v][0]=u; dept[v]=dept[u]+1;
		for(int j=1;j<=lg[dept[v]];++j)
			ff[v][j]=ff[ff[v][j-1]][j-1];
		dfs_pre(v,u);
	}
}
ll query_lca(ll a,ll b){
	if(a==b) return a;
	if(dept[a]<dept[b]) swap(a,b);
	for(int i=lg[dept[a]];i>=0;--i)
		if(dept[ff[a][i]]>=dept[b])
			a=ff[a][i];
	if(a==b) return a;
	for(int i=lg[dept[a]];i>=0;--i)
		if(ff[a][i]!=ff[b][i]){
			a=ff[a][i];
			b=ff[b][i];
		}
	return ff[a][0];
}
ll n,q,a[N],dis[N],val[N],siz[N]; //val记录边权,siz是点权和,dis记录路径权值 
ll f[N],g[N],dp[N];
//1为根时子树内的最大贡献/当前点的父亲走自己子树内的点且不走当前点子树时的最大贡献
//以i为起点能得到的最大贡献
void dfs1(ll u,ll fa){
	siz[u]=f[u]=a[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		dis[v]=dis[u]+w; val[v]=w;
		dfs1(v,u); //应该从叶子开始 
	}
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		f[u]+=max(f[v]-2*w,0ll);
	}
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		g[v]=f[u]-a[u]-max(f[v]-2*w,0ll);
	}
}
void dfs2(ll u,ll fa,ll ew){
	if(u!=1) dp[u]=max(dp[fa]-max(f[u]-2*ew,0ll)-2*ew,0ll)+f[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(v==fa) continue;
		g[v]+=g[u];
		siz[v]+=siz[u];
		dfs2(v,u,w);
	}
}
ll find_dis(ll u,ll v){
	ll dis=0;
	for(int i=lg[dept[u]];i>=0;--i)
		if(dept[ff[u][i]]>=dept[v]) u=ff[u][i],dis+=(1<<i);
	return dis; 
}
ll find_pos(ll u,ll k){ //倍增找点 
	for(int i=lg[dept[u]];i>=0;--i)
		if((k>>i)&1) u=ff[u][i];
	return u;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	for(int i=1;i<n;++i){
		ll u,v,w;
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);
	}
	dept[1]=1,dfs_pre(1,0);
	dfs1(1,0); dp[1]=f[1];
	dfs2(1,0,0);
	for(int i=1;i<=q;++i){
		ll u,v,lca,d,ans=0;
		cin>>u>>v; lca=query_lca(u,v);
		d=dis[u]+dis[v]-2*dis[lca];
		if(u==v) cout<<dp[u]<<'\n';
		else if(lca==u||lca==v){
			if(u==lca) swap(u,v); //默认u深度更大
			ll diss=find_dis(u,v),x=find_pos(u,diss-1);
			ans+=f[u]-a[u]+g[u]-g[x]+dp[v];
			ans-=max(f[x]-2*val[x],0ll);
			ans-=d; ans-=a[v];
			ans+=siz[u]-siz[ff[v][0]];
			cout<<ans<<'\n';
		}
		else{
			ll u2=find_pos(u,find_dis(u,lca)-1),v2=find_pos(v,find_dis(v,lca)-1);
			ans+=f[u]+f[v]-a[u]-a[v]-a[lca];
			ans+=g[u]+g[v]-g[u2]-g[v2];
			ans+=siz[u]+siz[v]-siz[lca]-siz[ff[lca][0]];
			ll x=max(f[u2]-2*val[u2],0ll),y=max(f[v2]-2*val[v2],0ll);
			ans+=dp[lca]-d-x-y;
			cout<<ans<<'\n';
		}
	}
	return 0;
}

AT dp_t

一眼没思路,但是较简单。考虑设 \(dp_{i,j}\) 为dp到第 \(i\) 个位置并且填 \(j\) 时的方案数。往后转移时为了保证数列是排列我们可以将 \(\ge j\) 的数都加一。我们枚举第 \(i-\mathbf{\small{1}}\) 个数(设为 \(k\))那么转移便是 \(dp_{i,j}=\bigg\lbrace{\large{\sum_{k=\mathbf{1}}^{j-\mathbf{1}}dp_{i-\mathbf{1},k}\quad,s_{i-\mathbf{1}}='<'} \atop \large{\sum_{k=j}^{i-\mathbf{1}}dp_{i-\mathbf{1},k}\quad,s_{i-\mathbf{1}}='>'}}\),时间复杂度 \(O(n^{\mathbf{\small{3}}})\)。考虑优化掉枚举 \(k\) 的那一步,我们发现其实这个式子中含 \(k\) 的那部分其实就是个前缀和,那就直接空间换时间就行了。这种题看着挺眼熟的。某人的话:一开始是 \(k\),然后减去所有的 \(\mathbf{\small{0}}\) 的个数再加上左边的 \(\mathbf{\small{0}}\) 的个数两倍,就是左边 \(\mathbf{\small{0}}\) 个数减去右边 \(\mathbf{\small{0}}\) 个数了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3001,M=1919810,mod=1e9+7;
ll n; bool f[N];
ll dp[N][N]; //dp到第i位,第i位填j
ll sum[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i=1;i<n;++i){
		char ch; cin>>ch;
		f[i]=(ch=='<');
	}
	for(int i=1;i<=n;++i) dp[1][i]=sum[1][i]=1;
	for(int i=2;i<=n;++i)
		for(int j=1;j<=i;++j){
			if(f[i-1]) dp[i][j]+=sum[i-1][j-1],dp[i][j]%=mod;
			else dp[i][j]+=(sum[i-1][i-1]-sum[i-1][j-1]+mod)%mod,dp[i][j]%=mod;
			sum[i][j]=sum[i][j-1]+dp[i][j],sum[i][j]%=mod;
		}
	ll ans=0;
	for(int i=1;i<=n;++i) ans+=dp[n][i],ans%=mod;
	cout<<ans%mod;
	return 0;
} 

P8548

带限制01背包。先一遍正常二维体积01背包,但是这里第二维的限制是没有上界的,直接转移就爆了。看到询问的 \(f\le \mathbf{\small{500}}\),考虑将第二维大于 \(\mathbf{\small{500}}\) 的dp值都压到 \(dp[j][\mathbf{501}]\) 的位置。然后需要 \(O(\mathbf{1})/O(logn)\) 的时间内查询,我们对于两个限制分开考虑,先按 \(f\) 从大到小找到最大值,这样子可以保证我们找到的满足限制,然后再去找到前缀最大值(和后缀最大值比较)。不过很奇怪的是必须要初始化成极小值,要不然就有错解。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=501,M=1919810;
ll n,q;
ll w[N],f[N],v[N],dp[N][N];
ll pre[N][N],suf[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;++i) cin>>w[i]>>f[i]>>v[i];
	for(int i=1;i<=n;++i)
		for(int j=N-1;j>=w[i];--j){
			for(int k=N;k>=N-f[i];--k)
				dp[j][N]=max(dp[j][N],dp[j-w[i]][k]+v[i]);
			for(int k=N-1;k>=f[i];--k)
				dp[j][k]=max(dp[j][k],dp[j-w[i]][k-f[i]]+v[i]);
		}
	for(int i=0;i<N;++i)
		for(int j=N;j>=0;--j)
			suf[i][j]=max(suf[i][j+1],dp[i][j]);
	for(int i=0;i<N;++i)
		for(int j=0;j<=N;++j)
			pre[i][j]=max(pre[max(i-1,0)][j],suf[i][j]);
	for(int i=1;i<=q;++i){
		ll c,f;
		cin>>c>>f;
		cout<<pre[c][f]<<'\n';
	}
	return 0;
} 

P8563

分类讨论。区间长度大于等于 \(\mathbf{\small{62}}\) 时直接输出太大,易证;没有负数或偶数个负数直接乘;有奇数个负数取第一个负数右边乘积和最后一个负数左边乘积较大值。不利用长区间直接输出的性质也可以线段树做,复杂一点罢了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll __int128
const ll N=2*114514,M=1919810,inf=1073741824;
ll n,q,a[N];
inline ll read(){
    ll 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;
}
inline void write(ll x){
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+'0');
    return;
}
void solve(ll l,ll r){
	ll p1=0,p2=0,cnt=0,sum=1;
	for(int i=l;i<=r;++i){
		sum*=a[i];
		if(sum>inf){
			cout<<"Too large\n";
			return;
		}
		if(a[i]<0){
			if(!p1) p1=i;
			p2=i; ++cnt;
		}
	}
	if((!p1&&!p2)||!(cnt&1)) write(sum),cout<<'\n';
	else if(p1==p2){
		ll s1=1,s2=1;
		for(int i=l;i<p1;++i){
			s1*=a[i];
			if(s1>inf){
				cout<<"Too large\n";
				return;
			}
		}
		for(int i=p1+1;i<=r;++i){
			s2*=a[i];
			if(s2>inf){
				cout<<"Too large\n";
				return;
			}
		}
		write(max(s1,s2)),cout<<'\n';
	}
	else{
		ll s1=1,s2=1,s3=1,s4=1;
		for(int i=l;i<p1;++i){
			s1*=a[i];
			if(s1>inf){
				cout<<"Too large\n";
				return;
			}
		}
		for(int i=p1+1;i<=r;++i){
			s2*=a[i];
			if(s2>inf){
				cout<<"Too large\n";
				return;
			}
		}
		for(int i=l;i<p2;++i){
			s3*=a[i];
			if(s3>inf){
				cout<<"Too large\n";
				return;
			}
		}
		for(int i=p2+1;i<=r;++i){
			s4*=a[i];
			if(s4>inf){
				cout<<"Too large\n";
				return;
			}
		}
		write(max(max(max(s1,s2),s3),s4)),cout<<'\n';
	}
}
int main(){
	//ios::sync_with_stdio(0);
	//cin.tie(0); cout.tie(0);
	n=read(),q=read();
	for(int i=1;i<=n;++i) a[i]=read();
	while(q--){
		ll opt,l,r;
		opt=read(),l=read(),r=read();
		if(opt==1) a[l]=r;
		else{
			if(r-l+1>100) cout<<"Too large\n";
			else solve(l,r);
		}
	}
	return 0;
} 

P8595 一个网的路

发现这个题的转移依赖于树的具体形态,考虑根据点和儿子之间不同的连边关系设状态。设 \(dp_{u,\mathbf{\small{0/1/2}}}\) 分别表示 \(u\) 被炸、\(u\) 没炸但只连一个儿子、\(u\) 没炸但只连两个儿子时的最小操作次数。虽然给的图是森林,但是珂以简单得出把每棵树分开变成链再接到一起是最优的。对于 \(\mathbf{\small{1/2}}\) 情况,我们肯定是考虑连上 \(u\) 的两个操作次数最大的儿子。现在考虑转移,\(dp_{u,\mathbf{\small{0}}}\) 可以从每个儿子的 \(\mathbf{\small{0/2}}\) 转移来并取较小值之和,注意是dp[u][0]+=min(dp[v][0]-1,dp[v][2])有一个减一,因为这个时候 \(u,v\) 是不连边的;\(dp_{u,\mathbf{\small{1}}}=\sum dp_{v,\mathbf{\small{0}}}-s_{\mathbf{\small{1}}},dp_{u,\mathbf{\small{2}}}=\sum dp_{v,\mathbf{\small{0}}}-s_{\mathbf{\small{1}}}-s_{\mathbf{\small{2}}}\)\(s_{\mathbf{\small{1}}},s_{\mathbf{\small{2}}}\) 是保留操作次数最大的两个儿子的收益,即为 \(dp_{v,\mathbf{\small{0}}}-dp_{v,\mathbf{\small{1}}}\)

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=20*114514,M=1919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,m,f[N],rt[N],rcnt;
ll dp[N][3],du[N],ans=0;
//炸/1个儿子/2个儿子 
ll find(ll x){
	return x==f[x]?x:f[x]=find(f[x]);
}
void dfs(ll u,ll fa){
	ll s1=0,s2=0;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][0]-1,dp[v][2]);
		dp[u][1]+=dp[v][0],dp[u][2]+=dp[v][0];
		if(dp[v][0]-dp[v][1]>s1) s2=s1,s1=dp[v][0]-dp[v][1];
		else s2=max(s2,dp[v][0]-dp[v][1]);
	}
	dp[u][0]+=du[u]+1;
	dp[u][1]-=s1;
	dp[u][2]-=s1+s2;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) f[i]=i;
	for(int i=1;i<=m;++i){
		ll a,b;
		cin>>a>>b; f[b]=a;
		++du[a],++du[b];
		add(a,b),add(b,a);
	}
	for(int i=1;i<=n;++i)
		if(find(i)==i) rt[++rcnt]=i;
	for(int i=1;i<=rcnt;++i){
		dfs(rt[i],0);
		ans+=min(dp[rt[i]][0],dp[rt[i]][2]);
	}
	ans+=rcnt-1;
	cout<<ans;
	return 0;
}

P5851 [USACO19DEC] Greedy Pie Eaters P

之前做过了但是理解不透彻,再做一次。首先这很明显是一个区间dp,我们考虑设 \(dp[l][r]\) 为吃区间 \([l,r]\) 派的最大收益。因为我们是左+\(k\)+右,所以转移式为 \(dp_{i,j}=max(dp_{i,k-\mathbf{1}}+val_k+dp_{k+\mathbf{1},j})\)。现在的问题是怎么求出这个 \(k\) 位置的贡献,我们同样考虑用区间dp的方式求出,重设为 \(val[i][j][k]\) 表示 \([i,j]\)\(k\) 位置的最大贡献,转移 \(val[i][j][k]=max(val[i+\mathbf{1}][j][k],val[i][j-\mathbf{1}][k])\)。就做完了,记得数组开大,至于区间越界便不用考虑了,反正都是 \(\mathbf{\small{0}}\) 不影响。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=305,M=1919810;
struct xx{
	ll w,l,r;
}a[M]; //开小了…… 
ll n,m,dp[N][N],val[N][N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;++i){ //虚空调试 
		cin>>a[i].w>>a[i].l>>a[i].r;
		for(int j=a[i].l;j<=a[i].r;++j)
			val[a[i].l][a[i].r][j]=max(val[a[i].l][a[i].r][j],a[i].w);
	}
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			for(int k=i;k<=j;++k)
				val[i][j][k]=max(max(val[i+1][j][k],val[i][j-1][k]),val[i][j][k]);
		}
	ll ans=0;
	for(int len=1;len<=n;++len)
		for(int i=1;i<=n-len+1;++i){
			ll j=i+len-1;
			for(int k=i;k<j;++k) dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]); 
			for(int k=i;k<=j;++k) dp[i][j]=max(dp[i][j],dp[i][k-1]+val[i][j][k]+dp[k+1][j]);
		}
	for(int i=1;i<=n;++i)
		for(int j=i;j<=n;++j)
			ans=max(ans,dp[i][j]);
	cout<<dp[1][n];
	return 0;
} 

P6594 换寝室

树形dp。想一想就能发现求的值是有单调性的,于是考虑二分答案。考虑设 \(dp[u][i][j]\) 为在 \(u\) 所在连通块中最大的点为 \(i\),最小的点为 \(j\) 时的老师不满意度,但很明显这个是 \(O(n^{\mathbf{\small{3}}})\) 的,尝试简化成 \(O(n^\mathbf{\small{2}})\) 的,发现其实不需要最大最小都枚举,只用枚举其中一个,如果两点的最小点相同说明他们在同一个连通块中。考虑怎么 \(O(n^{\mathbf{\small{2}}})\) 去check,首先对于每个点,找到所有能以他作为子树内最小点的根并标记下来,然后转移:\(dp_{u,i}=\sum\limits_{v\in son[u]}min(dp_{v,j}+cot,dp_{v,i})\) 其中 \(cot\) 表示把 \(v\) 加入连通块内所要增加的不满意值。感觉还有点没写完,鸽了

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=805,M=1919810,inf=1e9+7;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll dept[N],lg[N],f[N][32];
ll n,m,k,h[N],cot[N],dp[N][N];
void dfs_pre(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		f[v][0]=u;
		dept[v]=dept[u]+1;
		for(int j=1;j<=lg[dept[v]];++j)
			f[v][j]=f[f[v][j-1]][j-1];
		dfs_pre(v,u);
	}
}
ll query_lca(ll a,ll b){
	if(a==b) return a;
	if(dept[a]<dept[b]) swap(a,b);
	for(int i=lg[dept[a]];i>=0;--i)
		if(dept[f[a][i]]>=dept[b])
			a=f[a][i];
	if(a==b) return a;
	for(int i=lg[dept[a]];i>=0;--i)
		if(f[a][i]!=f[b][i])
			a=f[a][i],b=f[b][i];
	return f[a][0];
}
bool vis[N][N];
void dfs1(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs1(v,u);
		cot[u]+=cot[v];
	}
}
void dfs2(ll u,ll fa,ll val,ll p){
	vis[p][u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		if(h[p]>h[v]||h[v]-h[p]>val) continue;
		dfs2(v,u,val,p);
	}
}
void dfs3(ll u,ll fa){
	for(int i=1;i<=n;++i) dp[i][u]=(!vis[i][u]?inf:0);
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs3(v,u);
		ll minn=inf;
		for(int j=1;j<=n;++j) minn=min(minn,dp[j][v]);
		for(int j=1;j<=n;++j)
			if(dp[j][u]!=inf) dp[j][u]+=min(minn+cot[v],dp[j][v]);
	}
}
bool check(ll mid){
	ll ans=inf;
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j) vis[i][j]=0;
		dfs2(i,0,mid,i);
	}
	dfs3(1,0);
	for(int i=1;i<=n;++i) ans=min(ans,dp[i][1]);
	return ans<=k;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m>>k;
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	ll maxn=0,minn=1e9+7;
	for(int i=1;i<=n;++i) cin>>h[i],maxn=max(maxn,h[i]),minn=min(minn,h[i]);
	for(int i=1;i<n;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	dept[1]=1,dfs_pre(1,0);
	for(int i=1;i<=m;++i){
		ll x,y; cin>>x>>y;
		++cot[x],++cot[y];
		cot[query_lca(x,y)]-=2;
	}
	dfs1(1,0);
	ll l=0,r=maxn-minn,ans=0;
	while(l<=r){
		ll mid=l+r>>1;
		if(check(mid)) r=mid-1,ans=mid;
		else l=mid+1;
	}
	cout<<ans;
	return 0;
}

P2717

一个分治题。我们考虑做一次前缀和,每层分治计算出 \(mid\) 到每个位置的区间和与 \(len*k\) 的差,然后排序双指针求出。特别注意指针遍历顺序,一定要保证递减对递增序列,还有你的答案加的是哪一段区间的答案。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810;
ll n,k,a[N],b[N],sum[N],ans;
bool cmp(ll x,ll y){
	return x>y;
}
void calc(ll l,ll r){
	if(l==r){
		if(a[l]>=k) ++ans;
		return;
	}
	ll mid=l+r>>1;
	calc(l,mid),calc(mid+1,r);
	for(int i=l;i<=mid;++i) b[i]=sum[mid]-sum[i-1]-k*(mid-(i-1));
	for(int i=mid+1;i<=r;++i) b[i]=sum[i]-sum[mid]-k*(i-mid);
	sort(b+l,b+mid+1,cmp);
	sort(b+mid+1,b+r+1);
	ll j=mid+1;
	for(int i=l;i<=mid;++i){
		while(b[i]+b[j]<0&&j<=r) ++j;
		ans+=r-j+1;
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;++i)
		cin>>a[i],sum[i]=sum[i-1]+a[i];
	calc(1,n);
	cout<<ans;
	return 0;
}

CF995C

数学+分讨。2023春测T2翻版,答案考虑差分求出。由于指数 \(\ge\mathbf{\small{3}}\) 是可以直接暴力求出的,所以我们考虑先对答案加上一个 \(\sqrt{n}\),表示完全平方数的个数,然后去找指数更大的数,并且我们要注意一个数可能是多个指数的得数,答案珂能会有重复,开一个 map 记录。我们考虑去枚举指数和底数然后依次把得数扔进每个指数对应的 vector 里,这样它是单调的,求的时候直接二分出每个指数下有多少个数小于等于 \(n\) 就行了,注意是 upper_bound。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3*114514,M=1919810,mod=998244353,inf=1e18;
ll T;
map <ll,bool> ma;
vector <ll> g[114];
ll solve(ll n){
	if(!n) return 0;
	ll ans=0;
	ans+=sqrtl(n);
	for(int i=3;i<=log2(n)+1;++i)
		ans+=upper_bound(g[i].begin(),g[i].end(),n)-g[i].begin();
	return ans;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>T;
	for(int i=3;i<=log2(inf)+1;++i)
		for(int j=2;j<=1e6;++j){ //底数
			ll sum=1,f=0;
			for(int k=1;k<=i;++k){
				if(inf/sum<j){
					f=1;
					break;
				}
				sum*=j; //写成i了…… 
			}
			if(f) break;
			ll x=sqrtl(sum);
			if(x*x!=sum&&!ma[sum]) ma[sum]=1,g[i].push_back(sum);
		}
	while(T--){
		ll l,r;
		cin>>l>>r;
		cout<<solve(r)-solve(l-1)<<'\n';
	}
	return 0;
} 

CF1027F

先离散化,然后对每个任务的两天建无向边,这个图会是很多个连通块,考虑每个块内点数和边数的情况:若边数大于点数则一定无解;若边数等于点数即基环树的情况块的答案为最大的点,因为你要是想把答案变小那么就肯定要先做时间最大的任务,但是因为这种情况有环所以你最终都会回到那个最大的点上;若边数小于点数即为一棵树,取次大的点,可由上一种情况推来,没有环了自然就能使答案小一点,还有记得开大数组。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll int
#define in inline
const ll N=114514,M=2e6+5;
struct xx{
	ll next,to,id;
}e[2*M];
ll head[2*M],cnt;
void add(ll x,ll y,ll id){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].id=id;
	head[x]=cnt;
}
ll n,a[M],b[M],v[M],m;
bool vis[M],ma[M];
ll max1,max2,ecnt,pcnt;
void dfs(ll u,ll fa){
	if(vis[u]) return;
	vis[u]=1,++pcnt;
	if(u>max1) max2=max1,max1=u;
	else if(u>max2) max2=u;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,id=e[i].id;
		if(v==fa) continue;
		if(!ma[id]) ++ecnt;
		ma[id]=1; //记录下每条变的编号防止重复计数
		dfs(v,u);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>a[i]>>b[i];
		v[++m]=a[i],v[++m]=b[i];
	}
	sort(v+1,v+m+1);
	ll nm=unique(v+1,v+m+1)-v-1;
	for(int i=1;i<=n;++i){
		a[i]=lower_bound(v+1,v+nm+1,a[i])-v;
		b[i]=lower_bound(v+1,v+nm+1,b[i])-v;
		add(a[i],b[i],i),add(b[i],a[i],i);
	}
	ll ans=0;
	for(int i=1;i<=nm;++i){ //注意是离散化后的
		if(vis[i]) continue;
		max1=max2=ecnt=pcnt=0;
		dfs(i,0);
		if(ecnt>pcnt){
			cout<<-1;
			return 0;
		}
		if(ecnt==pcnt) ans=max(ans,v[max1]); //基环树 
		if(ecnt<pcnt) ans=max(ans,v[max2]); //树 
	}
	cout<<ans;
	return 0;
}

CF1036F

直接去推合法的数没啥规律,考虑正难则反,去看不合法的数。这个时候你就很容易发现不合法的数都是指数大于一的幂,那我们就可以考虑暴力枚举出所有的幂,然后把二次幂直接当 \(\sqrt{n}\) 减掉,其他的幂都放到一个数组里排序去重,每次 upper_bound 求个数就好了。不过也许是因为 vector 的特性,他去重之后会有一点元素剩在末尾没删掉,没删掉就出错了,还要特别 erase 一下。注意这个题的数据特别大乘的过程中会爆 long long,所以用__int128,还有大整数开根都用sqrtl(x)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll __int128
#define It vector<ll>::iterator
const ll N=114514,M=2e6+5;
inline ll read(){
    ll 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;
}
inline void write(ll x){
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+'0');
    return;
}
ll T;
ll qpow(ll a,ll b){
	ll ans=1;
	while(b){
		if(b&1) ans*=a;
		a*=a;
		b>>=1;
	}
	return ans;
}
vector <ll> g;
int main(){
	T=read();
	for(ll i=3;i<=60;++i)
		for(ll j=2;j<=1e6;++j){
			ll x=qpow(j,i);
			if(x>1e18) break;
			ll y=sqrtl(x);
			if(y*y!=x) g.push_back(x);
		}
	sort(g.begin(),g.end());
	It itr=g.end();
	It itl=unique(g.begin(),g.end());
	g.erase(itl,itr);
	while(T--){
		ll n=read(),ans=0;
		ans=n-(ll)sqrtl(n)-(upper_bound(g.begin(),g.end(),n)-g.begin());
		write(ans),putchar('\n');
	}
	return 0;
}

CF1065E

智慧题,首先和字符串半毛关系都没有。我们考虑将数列看成一条线段,画出每个 \(b_i\) 以及它关于中点的对称点。我们发现对于每一段 \([b_{i-\mathbf{1}},b_i],i\in[\mathbf{0},m)\) 而言我们都珂以将它和另一边与它对应的一段反转,除了 \(b_m\) 因为它包含了中点。我们将每一段分开考虑对答案的贡献:长度为 \(k\) 的一段方案数为 \(|A|^k\),另一边的与它对应且反转后不与原串相等的情况有 \(|A|-\mathbf{1}\) 种,并且要注意这一段翻过去前后的排列贡献都算了的,而他们实际上的贡献值为 \(\mathbf{2}\),比如 \(\mathbf{114},\mathbf{233}\)\(\mathbf{332},\mathbf{411}\) 实际上只有 \(\mathbf{2}\) 的贡献,所以每一段的贡献都要除以二,即 \(\dfrac{|A|^k\times(|A|^k-\mathbf{1})}{\mathbf{2}}\)。若与原串相等的话,原来另一边就只有一种情况与它对应,贡献就是 \(|A|^k\times \mathbf{1}\) 当然这个不除以二。还有对于最中间包含中点的一段,由于他无法反转贡献就是 \(|A|^{n-\mathbf{2}*b_m}\)。总的贡献也就每一段的贡献乘起来,当然以上的“段”都只是某一边的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810,mod=998244353;
ll n,m,A,b[N];
ll qpow(ll a,ll b){
	ll ans=1;
	while(b){
		if(b&1) ans=ans*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return ans;
}
ll ans=1;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m>>A;
	for(int i=1;i<=m;++i) cin>>b[i];
	for(int i=1;i<=m;++i){
		ll k=b[i]-b[i-1];
		ll x=(qpow(A,k)*(qpow(A,k)-1)%mod);
		x=x*qpow(2,mod-2)%mod;
		x=(x+(qpow(A,k))%mod)%mod; //推错可还行 
		ans=ans*x%mod;
	}
	ans=ans*qpow(A,n-2*b[m])%mod;
	cout<<ans;
	return 0;
}

ABC141E

把这个题放一下吧,主要是有个哈希的 trick,遇到这种要处理和区间相关的哈希值时我们珂以做一个哈希值前缀和,然后算一个对于不同区间长度时用的底数(珂以用前缀积的形式),然后对于一段区间的哈希值这么算:设左端点为 \(l\) 右端点为 \(r\),哈希值就是 (a[r]-a[l-1]*b[r-l+1]%mod+mod)%mod\(a\) 数组时哈希值前缀和,\(b\) 是对于不同区间长度的底数,反正就这么算。这个题中就二分答案然后 \(O(n^{\mathbf{2}})\) 暴力找,\(O(n^{\mathbf{2}}log n)\) 能过。

还有一件事,不写双哈希必死无疑。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810;
const ll base=1331,mod=998244353;
const ll base2=131,mod2=19260817;
ll n,a[N],b[N];
ll a2[N],b2[N];
string s;
ll gethash(ll l,ll r){
	return (a[r]-a[l-1]*b[r-l+1]%mod+mod)%mod;
}
ll gethash2(ll l,ll r){
	return (a2[r]-a2[l-1]*b2[r-l+1]%mod2+mod2)%mod2;
}
//看来不写双哈希必死 
bool check(ll mid){
	for(int i=1;i<=n;++i){
		if(i+mid-1>n) return 0;
		pair<ll,ll> x=make_pair(gethash(i,i+mid-1),gethash2(i,i+mid-1));
		for(int j=i+mid;j<=n;++j){
			if(j+mid-1>n) break;
			if(x==make_pair(gethash(j,j+mid-1),gethash2(j,j+mid-1))) return 1;
		}
	}
	return 0;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>s;
	b[0]=b2[0]=1;
	for(int i=1;i<=n;++i){
		a[i]=(a[i-1]*base+s[i-1])%mod;
		b[i]=(b[i-1]*base)%mod;
		a2[i]=(a2[i-1]*base2+s[i-1])%mod2;
		b2[i]=(b2[i-1]*base2)%mod2;
	}
	ll l=0,r=n,ans=0;
	while(l<=r){
		ll mid=l+r>>1;
		if(check(mid)) l=mid+1,ans=mid;
		else r=mid-1;
	}
	cout<<ans;
	return 0;
}

ABC150F

考虑将原序列转换一下,我们求一个差分序列,也就是 \(ac_i=a_i\oplus a_{i+\mathbf{1}}\)
鸽了

posted @ 2023-12-02 09:35  和蜀玩  阅读(45)  评论(0)    收藏  举报