HGOI7.19集训题解

#题解

讲道理,今天的题还是一如既往地没人性。虽然还是内部资料没有办法公布题面,我还是尽量概括吧…今天比昨天好一点…120分orz。


###第一题——城市轰炸(bomb)
【题目描述】

  • 给定 n n n个点 m m m条有向边,任意一条边的出点被轰炸时入点不能够被轰炸。求最少需要多少次才可以将这张图完全轰炸。

  • 讲道理,真的不喜欢大清早做这个图论题…早上没什么思路,打了把dfs草草结束,一份没拿。
  • 标准答案是利用tarjan求出强联通分量然后进行缩点。但是由于只给了512M(其实很大了orz),所以还是会爆栈。需要手开展打tarjan(没错是两个栈)。然后就变成了tarjan的模板题orz。
  • 为什么要缩点?因为任意一个点被轰炸之后,他边的入点就不能被轰炸。而一个强联通分量当中可以完全看做从任意一个点进入,另一个点出来的一条链。链上相隔就可以进行轰炸了。而且由于最长的路径的轰炸次数一定大于其他路径的轰炸次数,其他路径上的轰炸次数会被覆盖。
  • 那么在这个点上进行缩点,将点的个数赋给缩完点之后的点权。然后求最长权值的路径就是所轰炸的最长路径的次数。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
void fff(){
	freopen("bomb.in","r",stdin);
	freopen("bomb.out","w",stdout);
}
const int MAXN=1000100;
int nxt[MAXN],first[MAXN],en[MAXN],tot=0;
void add(int x,int y){
	nxt[++tot]=first[x];
	first[x]=tot;
	en[tot]=y;
}
int d[MAXN],f[MAXN],q[MAXN];
int nxt1[MAXN],first1[MAXN],en1[MAXN];
void add1(int x,int y){
	nxt1[++tot]=first1[x];
	first1[x]=tot;
	en1[tot]=y;
	d[y]++;
}
int n,m;
bool vis[MAXN],bz[MAXN];
int stack[MAXN],MS[MAXN],dfn[MAXN];
int g[MAXN],low[MAXN],r[MAXN],bel[MAXN];
int top,num;
void tarjan(int x){
	MS[MS[0]=1]=x;
	while (MS[0]){
		if(!dfn[x=MS[MS[0]]]){
			dfn[x]=low[x]=++tot;
			r[stack[++top]=x]=first[x];
			bz[x]=vis[x]=true;
		}
		bool enter=false;
		for (int i=r[x];i;i=nxt[i]){
			if(!vis[en[i]]){
				r[x]=nxt[i];
				MS[++MS[0]]=en[i];
				enter=true;
				break;
			}else{
				if(bz[en[i]]){
					low[x]=min(low[x],dfn[en[i]]);
				}
			}
		}
		if(enter) continue;
		if(dfn[x]==low[x]){
			num++;
			do{
				bel[stack[top]]=num;
				bz[stack[top--]]=false;
			}while(stack[top+1]!=x);
		}
		low[MS[--MS[0]]]=min(low[MS[MS[0]]],low[x]);
	}
}

int ans=0;
int main(){
	fff();
	scanf("%d%d",&n,&m);
	for (int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	tot=0;
	for (int i=1;i<=n;i++){
		if(!vis[i]) tarjan(i);
	}
	tot=0;
	for(int i=1;i<=n;i++){
		g[bel[i]]++;
		for (int j=first[i];j;j=nxt[j])
			if(bel[i]^bel[en[j]]) add1(bel[i],bel[en[j]]);
	}
	int l=0,r=0;
	for (int i=1;i<=n;i++){
		if(!d[i])f[q[++r]=i]=g[i];
	}
	while (l<r){
		int x=q[++l];
		if(!first1[x]) ans=max(ans,f[x]);
		for (int i=first1[x];i;i=nxt1[i]){
			f[en1[i]]=max(f[en1[i]],f[x]+g[en1[i]]);
			if(!--d[en1[i]]) q[++r]=en1[i];
		}
	}
	printf("%d\n",ans);
	return 0;
}

###第二题——密室(room)
【题目描述】

  • 给出一幅图,有n个点和m条有向边,对于每一条边都有经过的条件称为key。图上存在k( k ≤ 10 k\leq10 k10)种key,每个点有若干种,每条路径需要有若干种,key可以叠加,使用后不会消失。求从点1出发后到点n需要的最短路径是多少(边权为1)

  • 一看有叠加,k又这么小就知道要用二进制压位。然后类似于dp的做法吧…图上dp…orz
  • 这道题我刚开始打错了…打了把spfa还没有记录状态…但拿了95分…我也不知道为什么数据这么水…
  • 在想正解的时候考虑图上dp如何优化搜索次数…爆搜估计会爆…但…真的没有爆orz。
  • OK其实就一把粗暴的bfs(类似于spfa,更新最短路),存入队列记录一下压缩的钥匙个数。就做好了…这个比第一题简单不要太多…

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#define INF 0x3f3f3f
using namespace std;
void fff(){
	freopen("room.in","r",stdin);
	freopen("room.out","w",stdout);
}
struct Edge{
	int x,y;
	int key;
};
struct node{
	int x,key;
};
const int MAXN=5005;
vector <Edge> edge;
vector <int> G[MAXN];
int n,m,k;
int key_map[MAXN];
int dist[MAXN][5000];
bool visited[MAXN];
queue <node> q;
void bfs(){
	int ans=INF;
	q.push((node){1,key_map[1]});
	memset(dist,INF,sizeof(dist));
	dist[1][key_map[1]]=0;
	while (!q.empty()){
		node &e=q.front();q.pop();
		int siz=G[e.x].size();
		for (int i=0;i<siz;i++){
			Edge e1=edge[G[e.x][i]];
			if((e.key&e1.key)==e1.key&&dist[e.x][e.key]+1<dist[e1.y][e.key|key_map[e1.y]]){
				dist[e1.y][e.key|key_map[e1.y]]=dist[e.x][e.key]+1;
				if(e1.y==n) ans=min(ans,dist[e1.y][e.key|key_map[e1.y]]);
				q.push((node){e1.y,e.key|key_map[e1.y]});
			}
		}
	}
	if(ans<INF) printf("%d",ans);
	else printf("No Solution");
}
int main(){
	fff();
	scanf("%d%d%d",&n,&m,&k);
	for (int i=1;i<=n;i++){
		for (int j=1;j<=k;j++){
			int t;
			scanf("%d",&t);
			key_map[i]=key_map[i]|(t<<j);
		} 
	}
	for (int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		Edge e;
		e.x=x,e.y=y;
		e.key=0;
		for (int j=1;j<=k;j++){
			int t;
			scanf("%d",&t);
			e.key=e.key|(t<<j);
		}
		edge.push_back(e);
		G[x].push_back(edge.size()-1);
	}
	bfs();
	return 0;
}

###第三题——序列(seq)
【题目描述】

  • 给出长为n的序列,m个查询,求出查询区间 [ l , r ] [l,r] [l,r]内的最长连续值域。

  • 首先知道,在区间当中,值域随着区间的大小增大而增大或保持不变,具有单调性。但查询数量实在是太多,所以需要用莫队优化(不懂百度啊…)查询次数,减少对同一个元素的扫描次数。
  • 但是…传统的莫队会爆…大概是排序的时候出现了 [ 1 , 2 ] , [ 1 , 99999 ] , [ 2 , 2 ] , [ 2 , 99999 ] . . . [1,2],[1,99999],[2,2],[2,99999]... [1,2],[1,99999],[2,2],[2,99999]...这种沙雕变态数据吧…那我们换个方向存,求下每一个左端点所属的分块,然后先按照左端点的分块排,在按照右端点的单调递增进行排序…这样子左端点所属块每次最多扫到 n \sqrt n n ,最多就 m n m\sqrt n mn ,而右端点是单调递增的,所以每块的查询在右端点的查询次数不会超过 n n n。所以平均下大概就可以过了吧…
  • 然后怎么实现连续值域的求取呢…利用一个bool数组mark,进行标记,再利用左右端点指针进行连续性传递,剪一剪就出来了…然后和最大值比较下,得出答案。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
void fff(){
	freopen("seq.in","r",stdin);
	freopen("seq.out","w",stdout);
}
const int MAXN=50100;
int n,m,blo_siz;
int a[MAXN],t[MAXN];
struct node{
	int l,r,id;
	bool operator <(const node x)const{
		if((l-1)/blo_siz==(x.l-1)/blo_siz) return r<x.r;
		return (l-1)/blo_siz<(x.l-1)/blo_siz;
	}
}q[MAXN];
int now,lcur,rcur,cnt;
bool mark[MAXN];
int ans[MAXN];
int lp[MAXN],rp[MAXN];
int did[MAXN][3];

void insert_(int x,bool flag){
	if(lcur<=rcur){
		int delta=0,y;
		x=a[x];
		mark[x]=true,lp[x]=rp[x]=x;
		if(flag) cnt++,did[cnt][0]=did[cnt][1]=did[cnt][2]=x;
		if(x&&mark[x-1]){
			delta+=x-lp[x-1];
			lp[x]=lp[x-1];
			rp[lp[x-1]]=x;
			flag?did[cnt][0]=lp[x-1]:0;
		}
		if(x+1<=n&&mark[x+1]){
			y=lp[x];
			delta+=rp[x+1]-x;
			rp[y]=rp[x+1];
			lp[rp[x+1]]=y,flag?did[cnt][1]=rp[x+1]:0;
		}
		now=max(now,delta+1);
	}
}
void recover(int u){
	int x=did[u][2],y=did[u][0],z=did[u][1];
	mark[x]=false;
	if(y<x) rp[y]=x-1,lp[x-1]=y;
	if(x<z) rp[x+1]=z,lp[z]=x+1;
}
void solve(){
	int tmp,tail;
	for (int i=1;i<=m;i++){
		if(i==1||(q[i-1].l-1)/blo_siz!=(q[i].l-1)/blo_siz){
			memset(mark,0,sizeof(mark));
			tail=min(((q[i].l-1)/blo_siz+1)*blo_siz,n)+1;
			now=0,lcur=tail,rcur=0;
		}
		while (rcur<q[i].r) 
			insert_(++rcur,0);
		cnt=0,tmp=now;
		while (lcur>q[i].l) 
			insert_(--lcur,1);
			
		ans[q[i].id]=now;
		now=tmp;
		lcur=tail;
		
		while (cnt) 
			recover(cnt--);//还原左端点 
	}
	
	
}
int main(){
	fff();
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for (int i=1;i<=m;i++){
		
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	blo_siz=sqrt(n)+1;
	sort(q+1,q+m+1);
	solve();
	for (int i=1;i<=m;i++){
		printf("%d\n",ans[i]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值