26年国庆备课

P2919

简要题意:

给定一个 \(N\times M\) 的矩阵,每个元素 \(H_{ij}\) 表示高度,求有多少个山顶。一个山顶由一个或多个相邻且具有相同值的矩阵元素组成,这些元素被地图的边缘或具有较低(较小)高度的元素完全包围。

思路

其实就是遍历整张图,没有访问过的就 \(BFS\) 去搜索,然后按照要求模拟即可。

代码
/*@author:decentz*/
#include<bits/stdc++.h>
#define FRE(x,y) freopen(#x".in","r",stdin);freopen(#y".out","w",stdout);
#define LK_TIME cerr<<(1.0*clock()/CLOCKS_PER_SEC)<<'\n';
#define REP(i,a,b) for(int i=(a),i##end=(b);i<=i##end;i++)
#define PRE(i,a,b) for(int i=(a),i##end=(b);i>=i##end;i--)
#define MMS(arr,n) memset(arr,n,sizeof(arr));
#define DEBUG(x) cerr<<#x<<": "<<x<<'\n';
using namespace std;
using LL=long long;
const LL INF=LLONG_MAX,_INF=LLONG_MIN;
bool STWZZ;
const int dx[8]={-1,-1,-1,0,0,1,1,1},dy[8]={-1,0,1,-1,1,-1,0,1};
int N,M,H[710][710],ans;
bool vis[710][710];
bool EDWZZ;
int main(){
	//FRE(,);
	//cerr<<(&EDWZZ-&STWZZ)/1024.0/1024.0<<'\n';
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>N>>M;
	REP(i,1,N){
		REP(j,1,M){
			cin>>H[i][j];
		}
	}
	REP(i,1,N){
		REP(j,1,M){
			if(!vis[i][j]){
				queue<pair<int,int>> q;
				q.push({i,j});
				vis[i][j]=true;
				bool pd=1;
				while(q.size()){
					int x=q.front().first,y=q.front().second;q.pop();
					REP(d,0,7){
						int nx=x+dx[d],ny=y+dy[d];
						if(nx>=1&&nx<=N&&ny>=1&&ny<=M){
							if(H[nx][ny]>H[i][j]) pd=false;
							if(H[nx][ny]==H[i][j]&&!vis[nx][ny]){
								vis[nx][ny]=true;
								q.push({nx,ny});
							}	
						}
					}
				}
				if(pd) ans++;
			}
		}
	}
	cout<<ans;
	return 0;
}

P1268

简要题意:

给定一个 \(n\) 个叶节点的树,所有叶节点两两之间的最短距离已知(以距离矩阵的上三角形式给出)。求这棵树的总边权之和(即“树的重量”,是唯一确定的)。

思路

首先显然 (obviously) 这是一道构造题。
下面我们思考:

如果只有 $2$ 个点,答案是啥?

因为树只有一条边,所以显然为 \(dis_{1,2}\)。

那 $3$ 个点时,新点咋放?

显然要插在 \(1\) 和 \(2\) 的路径上。并且新增的边权为 $\frac{dis_{1,3}+dis_{2,3}-dis_{1,2}}{2} $(咋来的?因为 \(dis_{1,3}+dis_{2,3}\) 把1-2之间的那段算了两次,剪掉后正好是分支长度的两倍,如图)。

那更多的点呢?咋插?

假如当第四个点插入时,那么它可以插在1-2间,也可以插到1-3之间,也可以是2-3,ber,那咋整?插哪啊???

嘶,稍加思考,我们可以想到,直接枚举所有已经加入树中的点对 \((u,v)\),假设新点挂在u-v路径上,计算出新增的边权:\(\frac{dis_{u,i}+dis_{v,i}-dis_{u,v}}{2}\),然后去最小值就行。

不过貌似复杂度有点高啊(\(O(n^{3})\)),那能不能只枚举一个端点,我们再次观察一下上面的公式。假如固定 \(u=1\),变成这样:\(\frac{dis_{1,i}+dis_{v,i}-dis_{1,v}}{2}\)。我们考虑,这样真的行吗???

我们注意到 \(dis_{1,i}+dis_{v,i}-dis_{1,v}\) 这个式子,在 \(v\) 取遍所有已加入点时,得到的最小值就是点 \(i\) 到当前树的“最短分支长度”。其他点对 \((u,v)\) 算出的值一定不小于这个最小值(因为 \(1\) 号点已经是最早加入的,它的位置具有代表性)。时间复杂度降为 \(O(n^{2})\)。

至此,此题思考完毕!

代码
/*@author:decentz*/
#include<bits/stdc++.h>
#define FRE(x,y) freopen(#x".in","r",stdin);freopen(#y".out","w",stdout);
#define LK_TIME cerr<<(1.0*clock()/CLOCKS_PER_SEC)<<'\n';
#define REP(i,a,b) for(int i=(a),i##end=(b);i<=i##end;i++)
#define PRE(i,a,b) for(int i=(a),i##end=(b);i>=i##end;i--)
#define MMS(arr,n) memset(arr,n,sizeof(arr));
#define DEBUG(x) cerr<<#x<<": "<<x<<'\n';
using namespace std;
using LL=long long;
const LL INF=LLONG_MAX,_INF=LLONG_MIN;
bool STWZZ;
int n,dis[32][32],ans;
bool EDWZZ;
int main(){
	//FRE(,);
	//cerr<<(&EDWZZ-&STWZZ)/1024.0/1024.0<<'\n';
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n;
	REP(i,1,n){
		REP(j,i+1,n){
			cin>>dis[i][j];
			dis[j][i]=dis[i][j];
		}
	}
	ans=dis[1][2];//一开始只有两个点
	REP(i,3,n){
		int len=INT_MAX;
		REP(v,1,i-1) len=min(len,(dis[1][i]+dis[v][i]-dis[1][v])/2);//按照公式~
		ans+=len;
	}
	cout<<ans;
	return 0;
}

P8660

简要题意:

有 \(n\) 个区间 \(\left [ a_{i},b_{i} \right ]\),你可以把它们整体左右平移,平移量绝对值不超过 \(X\)。要求平移后这些区间的并集能完全覆盖 \(\left [ 0,10000 \right ]\)。求最小的 \(X\)。

思路

首先,为了避免精度误差,我们给每个区间左右端点都乘以 \(2\),答案也乘以 \(2\),最后除以 \(2\) 即可。

如何判断某一个 $X$ 是否可行呢?

首先我们知道左端点的范围在 \(\left [ a_{i}-X,a_{i}+X \right ]\) 内,右端点的范围在 \(\left [ b_{i}-X,b_{i}+X \right ]\) 内。现在我们从 \(0\) 开始向右覆盖。并维护一个变量 \(cur\),表示当前已经覆盖到的最右位置(初始为 0)。

那么在所有还没用过的区间里,如果某个区间的左端点最小可能值 \(a_{i}-X\le cur\),说明它可以接到当前覆盖到的最右位置(即 \(cur\))的右边。并且如果想让此区间覆盖尽可能多的位置的话,肯定是让它的左端点尽量往右放,但不能超过 \(cur\)。如果 \(a_{i}\le cur\),左端点最多到 \(\min (a_{i}+X,cur)\),右端点就是到 \(\min (20000,左端点+(b_{i}-a_{i}))\)。如果 \(a_{i}> cur\),左端点只能到 \(cur\),右端点就是到 \(\min (20000,cur+(b_{i}-a_{i}))\)。我们贪心地选择能延伸最远的区间。但直接每次扫描所有区间太慢,所以可以用一个集合来优化。

Ok呀,接下来我们想,假设当前可用的区间中,有一个右端点很小(即 $b_{i}$ 小),另一个右端点很大。如果我们先选右端点大的,当前覆盖位置会跳得很远,那个右端点小的区间可能因为覆盖位置超过了 $b_{i}+X$ 而失效,导致浪费,那咋整?

我们可以在所有可用区间中,选择右端点 \(b_{i}\) 最小的区间,同时尽量把它的左端点向右移。这样当前覆盖位置会向右推进,而右端点大的区间仍然可用,并且可以在更靠右的位置开始,从而利用平移量 \(X\) 获得更大的总覆盖。

这样我们只需用一个 \(set\) 存储可用区间,按 \(b_{i}\) 排序,每次取 \(begin()\) 即 \(b_{i}\) 最小的即可。

那么如何维护可用区间呢?

用一个 \(vector\) 存储每个位置的事件,即在数轴上标记每个区间“开始可用”和“不再可用”的位置,扫描时在这些位置及时更新可用集合。正数表示加入,负数表示移除。

现在最后一个问题是怎么找最小的?

观察题目(最大值最小)不难想到可以二分答案。

以上是此题的全部思路~

代码
/*@author:decentz*/
#include<bits/stdc++.h>
#define FRE(x,y) freopen(#x".in","r",stdin);freopen(#y".out","w",stdout);
#define LK_TIME cerr<<(1.0*clock()/CLOCKS_PER_SEC)<<'\n';
#define REP(i,a,b) for(int i=(a),i##end=(b);i<=i##end;i++)
#define PRE(i,a,b) for(int i=(a),i##end=(b);i>=i##end;i--)
#define MMS(arr,n) memset(arr,n,sizeof(arr));
#define DEBUG(x) cerr<<#x<<": "<<x<<'\n';
using namespace std;
using LL=long long;
const LL INF=LLONG_MAX,_INF=LLONG_MIN;
bool STWZZ;
const int V=20000;
int n,a[20005],b[20005];
vector<int> g[20005];
bool pd(int x){
	REP(i,0,V) g[i].clear();
	//为每个区间生成事件:
	//在a[i]-x处加入集合(左端点最小可能值<=当前位置时可用)
	//在b[i]+x处移除集合(右端点最大可能值<=当前位置时失效)
	REP(i,1,n){
		g[max(0,a[i]-x)].push_back(i);
		g[min(V,b[i]+x)].push_back(-i);
	}
	set<pair<int,int>> st;
	for(int i=0,las=0;i<V;){//i为当前覆盖到的最右位置,las为上次处理事件的位置
		REP(j,las,i){//处理从las到i之间所有位置的事件
			for(int p:g[j]){
				if(p>0){
					st.insert({b[p],p});//加入区间p
				}else{//移除区间p(注意g中存的是负数)
					p=-p;
					if(st.find({b[p],p})!=st.end()) st.erase({b[p],p});
				}
			}
		}
		if(st.empty()) return false;//当前没有可用区间,则无法继续覆盖
		int p=st.begin()->second;//贪心:取右端点最小的区间
		st.erase(st.begin());
		int r;
		//计算使用区间p后能覆盖到的最远位置r
		if(a[p]<=i){
			int l=min(i,a[p]+x);
			r=min(V,l+b[p]-a[p]);
		}else{
			int l=i;
			r=min(V,l+b[p]-a[p]);
		}
		las=i+1,i=r;
	}
	return true;
}
bool EDWZZ;
int main(){
	//FRE(,);
	//cerr<<(&EDWZZ-&STWZZ)/1024.0/1024.0<<'\n';
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n;
	REP(i,1,n){
		cin>>a[i]>>b[i];
		a[i]<<=1;
		b[i]<<=1;
	}
	int l=0,r=V;
	while(l<r){
		int mid=(l+r)>>1;
		if(pd(mid)) r=mid;
		else l=mid+1;
	}
	cout<<l/2.0;
	return 0;
}

P3419

简要题意:

架子上有 \(n\) 辆车,地板最多放 \(k\) 辆。入按顺序玩 \(p\) 辆车,每次车如果在地板上,直接拿;如果不在花费 \(1\) 次拿到。当地板满了,就必须放回去一辆,再拿新的。求最少的拿车次数。

思路
首先,我们思考,如果地板满了,该换哪辆车,才能让未来从架子拿车的次数最少?

这里先直接给出答案:拿走下一次使用时间最晚的那辆车(当然la,如果以后再也不用了,那更应该拿走)。

解释亿下,大家感性理解一下:就是如果一辆车很快就要再次被玩,你把它放回架子,过一会儿又得从架子拿下来,白白多花一次(这不捣乱吗)。而如果拿走一辆很久以后才用的车,那在这段时间里它一直占着地板也没用,不如腾出位置给马上要用的车(然后我发现这个策略好像叫 Belady最佳页面置换算法,大家如果感兴趣可以自己研究一下,这里就不展开讲了)。

接着,我们需要在每一步都知道地板上每辆车的下一次使用时间,那么怎么才能知道每辆车下一次使用是在什么时候呢?

我们可以提前算出来。先按顺序记录每辆车出现的所有位置。然后对于序列中的第 \(i\) 个位置,它所对应的车 \(a_{i}\) 下一次出现的位置就是这辆车紧跟在 \(i\) 后面的那个位置。如果后面没了,就设为无穷大。即我们用数组 \(nxt_{i}\),表示第 \(i\) 次请求后,同一辆车下一次出现的位置。

那么问题来了,如何高效维护地板上的车并找到最晚使用的?

思考,我们需要一个东西来维护,就是那种能快速找到当前地板上“下一次使用时间最晚”的车,并且还支持删除和插入操作。

这里我们可以选择 \(set\),里面存“下一次使用时间,车的编号”。因为 \(set\) 自动按第一关键字排序,所以最后一个元素就是下一次使用时间最晚的车,直接删掉它就行。

即具体操作就是:

  • 如果车已经在地板上:更新它的下一次使用时间,也就是从 \(set\) 中插入新的,删除旧的。

  • 如果不在:拿车次数加 \(1\)。如果地板还满了,那就删除 \(set\) 最后一个元素(就是最应该删除的车);然后把新车插入到 \(set\)中。

结束。

代码
/*@author:decentz*/
#include<bits/stdc++.h>
#define FRE(x,y) freopen(#x".in","r",stdin);freopen(#y".out","w",stdout);
#define LK_TIME cerr<<(1.0*clock()/CLOCKS_PER_SEC)<<'\n';
#define REP(i,a,b) for(int i=(a),i##end=(b);i<=i##end;i++)
#define PRE(i,a,b) for(int i=(a),i##end=(b);i>=i##end;i--)
#define MMS(arr,n) memset(arr,n,sizeof(arr));
#define DEBUG(x) cerr<<#x<<": "<<x<<'\n';
using namespace std;
using LL=long long;
const LL INF=LLONG_MAX,_INF=LLONG_MIN;
bool STWZZ;
const int N=500005;
int n,k,p,a[N],nxt[N],cur[N],ans;
bool in[N];
vector<int> pos[N];
set<pair<int,int>> s;//存储(下一次使用时间,车编号)
bool EDWZZ;
int main(){
	//FRE(,);
	//cerr<<(&EDWZZ-&STWZZ)/1024.0/1024.0<<'\n';
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n>>k>>p;
	REP(i,1,p){
		cin>>a[i];
		pos[a[i]].push_back(i);//存储这种车,在那些时候要玩
	}
	REP(i,1,n){
		REP(j,0,pos[i].size()-1){
			int id=pos[i][j];
			if(j+1<pos[i].size()) nxt[id]=pos[i][j+1];
			else nxt[id]=INT_MAX;
		}
	}
	REP(i,1,p){
		int x=a[i];
		if(in[x]){//车已在地板上,更新它的下一次使用时间
			s.erase({cur[x],x});
			cur[x]=nxt[i];
			s.insert({cur[x],x});
		}else{//车不在,需要拿下来
			ans++;
			if(s.size()==k){//地板满了,淘汰下一次使用最晚的车
				auto it=prev(s.end());
				in[it->second]=false;
				s.erase(it);
			}
			in[x]=true;
			cur[x]=nxt[i];
			s.insert({cur[x],x});
		}
	}
	cout<<ans;
	return 0;
}

P1053

简要题意:

有 \(n\) 个同学,一开始按 \(1,2,…,n\) 坐成一圈。每个同学希望自己旁边坐两个特定的人。你可以下达若干命令,每个命令选择一个环上的若干人,让他们按环的顺序轮换位置,代价是移动的人数。求让最终座位满足所有人愿望的最小总代价。如果无法满足,输出 \(-1\)。

思路

这道题本质上其实就是先看能不能围成一个满足所有人愿望的圈,如果可以,就找出这个目标圈,然后比较其与初始圈 \((1,2,…,n)\),通过操作目标圈,让尽量多的人待在原来位置上。最终的代价就是:\(n-最多不用动的人数\)。

思考,那怎么判断有没有解呢?目标圈怎么构造?

我们考虑,可以把每个人看成一个点,向他希望的两个人连边。

  • 如果某个点度数不是 \(2\),或者整张图不连通,或者从 \(1\) 出发走一圈不能经过所有人,就输出 \(-1\)。

  • 否则,就从 \(1\) 开始,随便选一个邻点作为下一个点,然后每一步都走“不是上一个点”的那个邻点,直到返回 \(1\)。这样得到的长为 \(n\) 的环 \(p\),就是唯一的目标圈。

怎么计算最多有多少人不用动呢?

首先初始圈是固定吧,即入 \(x\) 在位置 \(x\) 上。然后我们已经知道目标圈了,所以设 \(pos_{x}\) 为入 \(x\) 在目标圈中的位置。

  • 旋转目标圈(即整体的命令):假如整体移了 \(s\) 个位置,那么入 \(x\) 的新位置就是 \((pos_{x}+s)\bmod n\)。那么他不用动的条件也就出来了,即 \((pos_{x}-1+s)%n\)==\(x-1\),也就是 \(s \equiv x-pos_{i} \pmod{n} \)。之后我们统计所有入的这个差值,出现次数最多的那个差值,那么操作后肯定能让最多的人不动。所以这个最大次数就是此操作后的最大匹配数。
  • 翻转目标圈(即正常操作):翻转后入 \(x\) 的位置变为 \((n-pos_x)\bmod n\)。如果再整体旋转 \(s\) 个位置,那么他的新位置就是 \((n-pos_x+s)\bmod n\) 要使他不动,就需要 \((n-pos_x+s)\bmod n=x-1\),因此有 \(s\equiv x+pos_x-1\pmod n\)。我们同样统计每个差值出现的次数,出现次数最多的那个差值,就是翻转后最多能保持原位的人数。

最后,分别计算目标圈正向和反向时,经过最佳旋转后最多有多少人不动,取两者的最大值即可。最终答案即:\(n-最大值\)。

代码
/*@author:decentz*/
#include<bits/stdc++.h>
#define FRE(x,y) freopen(#x".in","r",stdin);freopen(#y".out","w",stdout);
#define LK_TIME cerr<<(1.0*clock()/CLOCKS_PER_SEC)<<'\n';
#define REP(i,a,b) for(int i=(a),i##end=(b);i<=i##end;i++)
#define PRE(i,a,b) for(int i=(a),i##end=(b);i>=i##end;i--)
#define MMS(arr,n) memset(arr,n,sizeof(arr));
#define DEBUG(x) cerr<<#x<<": "<<x<<'\n';
using namespace std;
using LL=long long;
const LL INF=LLONG_MAX,_INF=LLONG_MIN;
bool STWZZ;
const int N=50005;
int n,p[N],pos[N],cnt,ans1,ans2,hd[N],ver[2*N],nxt[2*N],tot;//p:目标环序列,pos[x]:入x在目标环中的位置
bool vis[N];//构造环时的访问标记
void add(int u,int v){
	ver[++tot]=v;nxt[tot]=hd[u];hd[u]=tot;
}
bool EDWZZ;
int main(){
	//FRE(,);
	//cerr<<(&EDWZZ-&STWZZ)/1024.0/1024.0<<'\n';
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n;
	REP(i,1,n){
		int u,v;cin>>u>>v;
		add(i,u);add(i,v);
	}
	//构造目标环
	int pre=0,cur=1;
	p[1]=1;//目标环第0个位置放1
	vis[1]=true;
	cnt=1;
	while(1){
		int nt=-1;
		for(int i=hd[cur];i;i=nxt[i]){// 在cur的两个希望邻居中找一个不等于pre的
			if(ver[i]!=pre){
				nt=ver[i];break;
			}
		}
		if(nt==-1){//找不到下一个,无解
			cout<<-1;return 0;
		}  
		if(nt==1) break;//回到1,环闭合
		if(vis[nt]){//遇到已访问的点(非起点),说明不是简单环,无解
			cout<<-1;return 0;
		}  
		vis[nt]=true;
		p[++cnt]=nt;//加入目标环
		pre=cur;cur=nt;
	}
	if(cnt!=n){//环长度不等于n,说明有孤立点,无解
		cout<<-1;return 0;
	}       
	REP(i,1,n) pos[p[i]]=i;//记录每个同学在目标环中的位置
	// 计算旋转情况下的最大匹配数
	int ci[N];MMS(ci,0);//ci[d]统计差值d出现的次数
	REP(x,1,n){
		int d=(x-pos[x])%n;
		if(d<0) d+=n;
		ci[d]++;
	}
	REP(i,0,n-1) ans1=max(ans1,ci[i]);
	// 计算翻转情况下的最大匹配数
	MMS(ci,0);
	REP(x,1,n){
		int d=(x+pos[x]-1)%n;
		if(d<0) d+=n;
		ci[d]++;
	}
	REP(i,0,n-1) ans2=max(ans2,ci[i]);
	cout<<n-max(ans1,ans2);
	return 0;
}

(终于结束了…qwq)
posted @ 2026-10-02 21:12  decentz  阅读(30)  评论(1)    收藏  举报