25-暑期-来追梦noip-卷6 总结

开题顺序:B-C-D-A

分配时间:B 45min C 60min D 5min A 10min

A

赛时没有注意到是简单图。糖丸了 /ll

考虑到直接删边做是困难的。于是从点的角度考虑。

我们观察到 \(N\) 很小,于是考虑枚举点的编号的全排列,然后和小图一一对应判断是否合法(小图有的边大图也得有)。

然后统计方案时不能重复,于是用 map 记录一下边的使用情况即可(二进制存储)。

实际上是简单题,赛时以为巨大困难所以跳了。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=10;
int n,n1,n2,m1,m2;
bool ok[N][N];
int ok2[N][N],pos[N];
map<int,bool> vis;

signed main(){
	//freopen("T1.in","r",stdin);
	//freopen("T1.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n1>>n2>>m1>>m2;
	if(n1!=n2)
		cout<<0,exit(0);
	n=n1;
	for(int i=1,u,v;i<=m1;i++)
		cin>>u>>v,ok[u][v]=ok[v][u]=1;
	for(int i=1,u,v;i<=m2;i++)
		cin>>u>>v,ok2[u][v]=ok2[v][u]=i;
	for(int i=1;i<=n;i++)
		pos[i]=i;
	int ans=0;
	do{
		bool f=1;
		int tmp=0;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(ok[i][j]){
					if(!ok2[pos[i]][pos[j]])
						f=0;
					tmp|=(1<<ok2[pos[i]][pos[j]]);	
				}		
			}
		}
		if(f&&!vis[tmp])
			vis[tmp]=1,ans++;
	}while(next_permutation(pos+1,pos+n+1));
	cout<<ans;
	return 0;
}

总结:

  • 不要畏难,仔细读题。

  • 切换研究对象。

B

做过原题,所以秒了。直接开 \(50\) 棵树然后树上差分即可。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=3e5+5;
const int K=55,M=48;
const int MOD=998244353;
int n,m;
vector<int> G[N];
int f[N],dep[N][K],sum[N][K],dp[N][M],deep[N],deep2[N]; 

int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1)
			res=res*x%MOD;
		x=x*x%MOD,y>>=1;
	}
	return res%MOD;
}
void initLCA(int cur,int fa){
	dp[cur][0]=fa;
	deep[cur]=deep[fa]+1;
	for(int i=1;(1<<i)<=deep[cur];i++)
		dp[cur][i]=dp[dp[cur][i-1]][i-1];
	for(int i:G[cur]){
		if(i==fa)
			continue;
		deep2[i]=(deep2[cur]+1)%MOD;
		for(int j=1;j<=50;j++)
			dep[i][j]=qpow(deep2[i],j)%MOD,sum[i][j]=(sum[cur][j]+dep[i][j])%MOD;
		initLCA(i,cur);
	}
}
int getLCA(int x,int y){
	if(deep[x]<=deep[y])
		swap(x,y);
	for(int i=31;i>=0;i--)
		if(deep[dp[x][i]]>=deep[y])
			x=dp[x][i];
	if(x==y)
		return x;
	for(int i=31;i>=0;i--)
		if(dp[x][i]!=dp[y][i])
			x=dp[x][i],y=dp[y][i];
	return dp[x][0];
}

signed main(){
	//freopen("T2.in","r",stdin);
	//freopen("T2.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	initLCA(1,0);
	cin>>m;
	for(int i=1,u,v,k;i<=m;i++){
		cin>>u>>v>>k;
		int lca=getLCA(u,v);
		cout<<(((sum[u][k]+sum[v][k])%MOD-(2*sum[lca][k])%MOD+MOD)%MOD+dep[lca][k])%MOD<<'\n';
	}
	return 0;
}

C

很自然的想到按时间排序,不然会错过很多单,一定不优。

然后画一下可以发现我们选的单实际上是一个关于时间的最长上升子序列。然后就能设计出一个 \(\mathcal{O}(k^2)\) 的 dp,但这是无法接受的。

考虑优化。观察到图是很小的(仅仅 \(200\) 个点),说明很多单的地址是重复的,这意味着我们可以复用之前的 dp 值。具体的,对于第 \(i\) 单(\(i>200\)),它能从 \(dp_{i-200}\) 及之前的状态转移而来,维护一个 \(\max\) 转移即可。

这样,时间复杂度即为 \(\mathcal{O}(200k)\),可以通过。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5,M=2e2+5;
int n,m,k;
int dis[M][M],dp[N];
struct NODE{
	int t,p,val;
}a[N];

bool cmp(NODE &x,NODE &y){
	return x.t<y.t;
}
void floyd(){
	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}

signed main(){
	//freopen("T3.in","r",stdin);
	//freopen("T3.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m;
	memset(dis,0x3f,sizeof dis);
	for(int i=1;i<=n;i++)
		dis[i][i]=0;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		dis[u][v]=dis[v][u]=1;
	}
	floyd();
	cin>>k;
	for(int i=1;i<=k;i++)
		cin>>a[i].t>>a[i].p>>a[i].val;
	sort(a+1,a+k+1,cmp);
	for(int i=1;i<=k;i++)
		dp[i]=-1e18;
	a[0].p=1,a[0].t=0;
	int ans=-1e18,premx=-1e18;
	for(int i=1;i<=k;i++){
		if(i>200){
			premx=max(premx,dp[i-200]);
			dp[i]=premx+a[i].val;
		}
		for(int j=i-1;j>=0;j--){
			if(i-j>=200)
				break;
			if(a[j].t+dis[a[i].p][a[j].p]<=a[i].t)
				dp[i]=max(dp[i],dp[j]+a[i].val);
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}

总结:

  • dp 优化:观察数据范围、复用之前的状态。

D

将军要么向左拓展,要么向右拓展,且选的必须是连续区间,考虑区间 dp。

然后我们发现只带上区间 \([i,j]\) 是无法转移的,需要三维,只能考虑带上 \(k\),即尚未解救的部队个数。

最后,我们还需要记录将军停在哪里,所以还需要两个 \(dp1,dp2\)

因此,令 \(dp1_{i,j,k}\) 表示区间 \([i,j]\) 且停在 \(i\)、尚未解决的部队个数为 \(k\) 时能解救的最大士兵数,\(dp2\) 是类似的。

初始就是将军那个位置设为 \(0\),其余极大;答案 \(\max(dp1_{1,n,0},dp2_{1,n,0})\)

转移的话,分四种情况:解救/不解救 \(i/j\) 即可。这部分是简单的。

时间复杂度三次方。事实上,我认为本题最大的难点在于看出是区间 dp 以及状态设计。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=3e2+5;
int n;
struct A{
	int x,siz;
}a[N];
int dp1[N][N][N],dp2[N][N][N];

bool cmp(A &u,A &v){
	return u.x<v.x;
}

signed main(){
	//freopen("T4.in","r",stdin);
	//freopen("T4.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].siz;
		if(abs(a[i].x)>=a[i].siz)
			a[i].siz=0;
	}
	++n;
	sort(a+1,a+n+1,cmp);
	int st=-1;
	for(int i=1;i<=n;i++){
		if(!a[i].x&&!a[i].siz){
			st=i; break;
		}
	}
	memset(dp1,0xcf,sizeof dp1);
	memset(dp2,0xcf,sizeof dp2);
	for(int i=0;i<n;i++)
		dp1[st][st][i]=dp2[st][st][i]=0;
	for(int len=2;len<=n;len++){
		for(int i=1;i<=n-len+1;i++){
			for(int k=0;k<=n-len;k++){
				int j=i+len-1;
				dp1[i][j][k]=max(dp1[i][j][k],max(dp1[i+1][j][k]-k*(a[i+1].x-a[i].x),dp2[i+1][j][k]-k*(a[j].x-a[i].x)));
				dp1[i][j][k]=max(dp1[i][j][k],max(a[i].siz+dp1[i+1][j][k+1]-(k+1)*(a[i+1].x-a[i].x),a[i].siz+dp2[i+1][j][k+1]-(k+1)*(a[j].x-a[i].x)));
				dp2[i][j][k]=max(dp2[i][j][k],max(dp1[i][j-1][k]-k*(a[j].x-a[i].x),dp2[i][j-1][k]-k*(a[j].x-a[j-1].x)));
				dp2[i][j][k]=max(dp2[i][j][k],max(a[j].siz+dp1[i][j-1][k+1]-(k+1)*(a[j].x-a[i].x),a[j].siz+dp2[i][j-1][k+1]-(k+1)*(a[j].x-a[j-1].x)));
			}
		}
	}
	cout<<max(dp1[1][n][0],dp2[1][n][0]);
	return 0;
}

总结:

  • 根据数据范围设计状态。

结语

成绩:30+100+30+14=174。

问题:dp 方面,对于模型不敏感,对数据范围观察不仔细(可以用于设计状态优化);做题方面,有畏难情绪导致读题不仔细,对于 idea 没有记录下来。

方案:读题方面,仔细一些,注重数据范围。看到最优化或者求方案数马上想 dp。还有就是 深入拓展可能的 idea。

posted @ 2025-08-20 22:32  _KidA  阅读(12)  评论(0)    收藏  举报