noip 2017 做题记录

noip 2017

小凯的疑惑

最大不可表数。
以前写过 这篇博客

时间复杂度

恶心的模拟题……
推荐食用洛谷第一篇题解,写的很好。
getline 简要讲解
读入一个单词时,可用 cin/scanf。读入一行带空格的字符时,可用getline。(我只会这几个,而且它们用起来比较方便因为我太懒了 )。
我写的与洛谷题解稍有不同,因为他读入字符串的方法感觉很高级其实是看不懂

#include
using namespace std;

int t,n,fl,ok,a,b,st,pn,ans,p;
bool ins[105],ef[105];
string o,s[105];

int geto(){
	if(o[3]=='1') return 0;
	else{
		int p=5,res=0;
		while('0'<=o[p]&&o[p]<='9'){
			res=res*10+o[p]-'0';
			++p;
		}
		return res;
	}
}

void work(){
	ok=pn=ans=0;
	st=-1;
	memset(ins,0,sizeof(ins));
	memset(ef,0,sizeof(ef));
	stack<int>q;
	for(int i=1;i<=n;++i){
		if(s[i][0]=='F'){
			int x=s[i][2]-'a';
			if(ins[x]){
				ok=1;
				return;
			}
			q.push(x);ins[x]=1;
			p=4;
			a=b=0;
			while('0'<=s[i][p]&&s[i][p]<='9'){
				a=a*10+s[i][p]-'0';
				++p;
			}
			if(p==4) a=10000;
			++p;
			bool bj=0;
			while(!isdigit(s[i][p])&&s[i][p]!='n') ++p;
			while('0'<=s[i][p]&&s[i][p]<='9'){
				b=b*10+s[i][p]-'0';
				++p;bj=1;
			}
			if(!bj) b=10000;
			if(b-a>100&&st==-1){
				++pn;ans=max(ans,pn);
				ef[x]=1;
			}
			if(a>b&&st==-1) st=x;
		}
		else{
			if(q.empty()){
				ok=1;
				return;
			}
			int x=q.top();q.pop();
			ins[x]=0;
			if(x==st) st=-1;
			if(ef[x]){
				ef[x]=0;
				--pn;
			}
		}
	}
	if(!q.empty()) ok=1;
}

int main(){
	//freopen("input.txt","r",stdin);
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);getline(cin,o);
		fl=geto();
		for(int i=1;i<=n;++i) getline(cin,s[i]);
		work();
		if(ok) printf("ERR\n"); 
		else{
			if(ans!=fl) printf("No\n");
			else printf("Yes\n");
		}
	}
	return 0;
}

逛公园

30分数据:
当 k=0 时,问题转化为从1到 n 的最短路有多少条,转化为 最短路计数 这道题,只需在原有的spfa上稍作改动,在松弛节点时用sum数组计数:

if(d[v]>d[u]+1)
{
	d[v]=d[u]+1;
	sum[v]=sum[u]%mod;
	if(!inq[v])
	{
		inq[v]=1;
		q.push(v);
	}
}
else if(d[v]==d[u]+1) 
{
	sum[v]=(sum[u]+sum[v])%mod;
}

正解:
建反图跑一遍最短路,d[u] 表示从 u 到 n 的最短路长度。
记忆化搜索,f[u][r] 表示从 u 到 n 的路径中,与 d[u] 长度差不超过 r的路径数量。则最终答案即为 f[1][k] 。
对一个点 u,它的 f[u][r] 由每个出点 v 更新而来。而每条由 n 到 v 再到 u 的路径在经过边(u,v)后与 d[u] 的差会增加 d[v]+w(边(u,v)的长度)-d[u],所以限制了所有走到 v 的路径长度不得超过r-d[v]+w-d[u],故:

f[u][r] = ∑ f[v][r-d[v]+w-d[u]]。当 u=n 时f[u][r]=1。

因为图中有0环,所以用数组 in[u][r] 标记来判断由 u 是否会走到自己且路径长度不变。在搜索完 u 之后清除标记。
部分题解对0环的处理是错误的,建议在水完洛谷的经验后UOJ 提交程序以验证正确性。

#include
using namespace std;

int read(){
	int 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;
}

typedef long long ll;
const int maxn=1000005;
int t,n,m,k,P,cnt,tot;
int head[maxn],d[maxn],hed[maxn];
ll f[maxn][55];
bool vis[maxn],in[maxn][55];

struct edge{
	int v,nxt,w;
}e[maxn],g[maxn];

struct node{
	int u,dis;
	bool operator < (const node a)const{
		return dis>a.dis;
	} 
};

void add(int u,int v,int w){
	e[++cnt].nxt=head[u];
	e[cnt].v=v;
	e[cnt].w=w;
	head[u]=cnt;
}

void ad(int u,int v,int w){
	g[++tot].nxt=hed[u];
	g[tot].v=v;
	g[tot].w=w;
	hed[u]=tot;
}

void dijkstra(){
	priority_queue<node>q;
	memset(d,0x3f,sizeof(d));
	memset(vis,0,sizeof(vis));
	vis[n]=1;d[n]=0;
	q.push((node){n,0});
	while(!q.empty()){
		int u=q.top().u;
		q.pop();vis[u]=1;
		for(int i=hed[u];i;i=g[i].nxt){
			int v=g[i].v,w=g[i].w;
			if(d[v]>d[u]+w){
				d[v]=d[u]+w;
				if(!vis[v]) q.push((node){v,d[v]});
			}
		}
	}
}

int dfs(int u,int r){
	if(in[u][r]) return -1;
	if(f[u][r]) return f[u][r];
	if(u==n) f[u][r]=1;
	else f[u][r]=0;in[u][r]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v,w=e[i].w;
		int dif=d[v]+w-d[u];
		if(dif>r) continue;
		int tmp=dfs(v,r-dif);
		if(tmp==-1) return f[u][r]=-1;
		else f[u][r]=(f[u][r]+tmp)%P;
	}
	in[u][r]=0;
	return f[u][r];
}

int main(){
	//printf("%lf",(double)sizeof(f)/(1<<20));
	//freopen("input.txt","r",stdin);
	t=read();
	while(t--){
		cnt=tot=0;
		memset(head,0,sizeof(head));
		memset(hed,0,sizeof(hed));
		memset(f,0,sizeof(f));
		memset(in,0,sizeof(in));
		n=read(),m=read(),k=read(),P=read();
		for(int x,y,z,i=1;i<=m;++i){
			x=read(),y=read(),z=read();
			add(x,y,z);ad(y,x,z);
		}
		dijkstra();
		printf("%d\n",dfs(1,k));
	}
	return 0;
}

奶酪

做法一:并查集
用两个数组分别存储与上切面相交(相切),与下切面相交(相切)的空洞。在读入数据时将相交或相切的洞两两合并祖先。最后查询上述两个数组中是否有元素的并查集祖先相同,表明可以从下切面走到上切面。
(以前的马蜂有点丑虽然现在也不咋地

#include
#define read read()
#define ll long long
using namespace std;

ll t,tot1,tot2,ans;
ll n,h,r,x[2000+5],y[2000+5],z[2000];
ll d1[2000],d2[2000],f[2000];

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;
}

bool dis(ll a,ll b,ll c,ll d,ll e,ll f)
{
	if((a-d)*(a-d)+(b-e)*(b-e)+(c-f)*(c-f)<=(4*r*r))
	    return 1;
	else return 0;
}

int find(ll a)
{
	if(f[a]==a) return a;
	else return f[a]=find(f[a]);
}

void readdata()
{
	t=read;
	while(t--)
	{
		tot1=0;
		tot2=0;
		ans=0;
		n=read;h=read;r=read;
		//memset()
		for(int i=1;i<=n;i++)
			f[i]=i;
		for(int i=1;i<=n;i++)
		{
			x[i]=read;y[i]=read;z[i]=read;
		    if(z[i]<=r)
		    {
		    	d1[++tot1]=i;
			}
			if(z[i]+r>=h)
			{
				d2[++tot2]=i;
			}
			for(int j=1;j<i;j++)
			{
				if(dis(x[i],y[i],z[i],x[j],y[j],z[j]))
				    if(find(i)!=find(j))
				        f[find(i)]=find(j);
			}
		}
		for(int i=1;i<=tot1;i++)
		{
			for(int j=1;j<=tot2;j++)
			{
				if(find(d1[i])==find(d2[j])) {ans=1;break;}
			}
			if(ans==1) break;
		}
		if(ans==1) printf("Yes\n");
		else printf("No\n");
	}
}

int main()
{
	//freopen("in.txt","r",stdin);
	readdata();
	return 0;
}

做法二:搜索
从每个与下切面相交或相切的空洞出发,走向一个未被标记过的且二者距离小于等于二倍半径的空洞。若能走到一个与上切面相切或相交的空洞则返回。
这样做跑的很快,应该是因为用来标记节点的vis数组的功劳。

#include
using namespace std;

const int maxn=1000+5;
long long x[maxn],y[maxn],z[maxn],vis[maxn];
int t,n,h,r;
bool ok;

bool pd(int a,int b)
{
	return (x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b])+(z[a]-z[b])*(z[a]-z[b])<=4*(long long)r*r;
}

void dfs(int i)
{
	if(ok) return;
	vis[i]=true;
	if(z[i]>=h-r)
	{
		ok=true;
		return;
	}
	for(int j=1;j<=n;j++)
	{
		if(vis[j]==0 && pd(i,j))
		{
			dfs(j);
		}
	}
}

bool check()
{
	ok=false;
	memset(vis,0,sizeof(vis));
	for (int i=1;i<=n;i++)
	{
		if(vis[i]==0 && z[i]<=r)
		{
			dfs(i);
		}
	}
	return ok;
}

void readdata()
{
	int u,v;
	scanf("%d",&t);
	for (int I=1;I<=t;I++)
	{
		scanf("%d%d%d",&n,&h,&r);
		for (int i=1;i<=n;i++)
		{
			scanf("%lld%lld%lld",&x[i],&y[i],&z[i]);
		}
		if(check())
			printf("Yes\n");
		else
			printf("No\n");
	}
}


int main()
{
	readdata();
	return 0;
}

宝藏

45分:prim求最小生成树,按每个点的L×K,即 “道路的长度” 乘以 “赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量” 排序,贪心即可。

#include
using namespace std;

const int maxn=1005;
const int inf=0x3f3f3f3f;
int n,m,cnt,ans,sum;
int head[maxn];
bool vis[maxn];

struct edge{
	int v,nxt,w;
}e[maxn<<1];
struct node{
	int u,w,k;
	bool operator <(const node a) const{
		return w*k>a.w*a.k;
	}
};

void add(int u,int v,int w){
	e[++cnt].nxt=head[u];
	e[cnt].v=v;
	e[cnt].w=w;
	head[u]=cnt;
}

void bfs(int x){
	memset(vis,0,sizeof(vis));
	priority_queue<node>q;
	q.push((node){x,0,0});sum=0;
	while(!q.empty()){
		int u=q.top().u,d=q.top().w,k=q.top().k;q.pop();
		if(vis[u]) continue;
		vis[u]=1;sum+=k*d;
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].v,w=e[i].w;
			if(vis[v]) continue;
			q.push((node){v,w,k+1});
		}
	}
	ans=min(ans,sum);
}

int main(){
	freopen("input.txt","r",stdin);
	scanf("%d%d",&n,&m);
	for(int x,y,z,i=1;i<=m;++i){
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);add(y,x,z);
	}
	ans=inf;
	for(int i=1;i<=n;++i) bfs(i);
	printf("%d",ans);
	return 0;
}

100分:搜索剪枝
用二维数组 d 存边,由于可能有重边,所以取最小的边权。t 数组存每个节点所连的节点个数。tar 数组辅助存边,tar[u][i] 数组表示与 u 点相连的第 i 条边。预处理将tar 排序,使每个点所连的边的按从小到大的顺序以便贪心减少搜索量。
r 数组存搜索时已经经过的点。lev 数组存这些点的深度,即起点到该宝藏屋经过的宝藏屋数量。变量 tot 存当前答案。
用tmp存由当前点搜索下去的最小可能边权和,即剩下未经过的点中,各自所连的最短的边的权值和。这个变量是用来给搜索剪枝,到当前节点 u 时,最小的可能答案即为 tmplev[u],若 tot+tmplev[u] 已经大于了先前算出的答案,则直接返回。

大概模拟一下搜索过程:

设最终答案为这样一棵树,从一号点开始搜索noip 2017 做题记录_第1张图片
与它相连的点只有二号点,来到二号点
,noip 2017 做题记录_第2张图片
此时node为1,st为2,但1号点无其它连边,所以开始枚举2号点的子节点,到达3号点
noip 2017 做题记录_第3张图片
此时node为2,st为3,但此时2号点还有其他子节点,即tar[i][j]还有值,所以下一步搜索5号点。
noip 2017 做题记录_第4张图片
2号点已无子节点,搜素下一个node,即node=3的子节点,来到4号点。
noip 2017 做题记录_第5张图片

然后搜索到是6号点,搜索完成。

建议自己造组数据跑一遍。蒟蒻自己人脑模拟时总感觉答案情况跑不完。实际调试后才明白搜索顺序。

模拟后可知之后搜索到的点的深度一定不小于当前点,所以tmp*当前点的lev 值一定小于等于正确答案中剩余节点的所连边的边权值和乘以深度。这个剪枝是正确的。

(代码注释来自洛谷题解)

#include
using namespace std;

const int inf=0x3f3f3f3f;
int n,m,p,tmp,ans,tot,cnt;
int r[20],lev[20];
int d[20][20],t[20],tar[20][20];

bool cmp(int a,int b){
	return d[p][a]<d[p][b];//按照费用给每个点能到的点排序
}

void dfs(int num,int st){
	for(int s,i=num;i<=cnt;++i){//由第几个被访问的点来扩展
		s=r[i];
		if(tot+tmp*lev[s]>=ans) continue; //最优性剪枝,如果当前解加上理论最小的费用都比中途的答案高,那么这次搜索一定不是最优解
		for(int j=st;j<=t[s];++j){//下一步扩展谁
			if(lev[tar[s][j]]) continue;//用lev同时充当标记,有lev代表已访问
			lev[tar[s][j]]=lev[s]+1;
			r[++cnt]=tar[s][j];
			tmp-=d[tar[s][j]][tar[tar[s][j]][1]];//理论最小的更新
			tot+=d[s][tar[s][j]]*lev[s];
			dfs(num,j+1);//继续从i枚举, 注意到tar[i][1 ~ j]已全部访问,下一次从j + 1尝试访问
			lev[tar[s][j]]=0;
			--cnt;
			tmp+=d[tar[s][j]][tar[tar[s][j]][1]];
			tot-=d[s][tar[s][j]]*lev[s];//回溯 
		}
		st=1;
	}
	if(cnt==n) ans=min(ans,tot);
}

int main(){
	//freopen("input.txt","r",stdin);
	scanf("%d%d",&n,&m);
	memset(d,0x3f,sizeof(d));
	for(int x,y,z,i=1;i<=m;++i){
		scanf("%d%d%d",&x,&y,&z);
		if(d[x][y]==inf){
			tar[x][++t[x]]=y;
			tar[y][++t[y]]=x;
		}
		d[x][y]=d[y][x]=min(d[x][y],z);
	}
	for(int i=1;i<=n;++i){
		p=i;
		sort(tar[i]+1,tar[i]+t[i]+1,cmp);
		tmp+=d[i][tar[i][1]];
	}
	ans=inf;
	for(int i=1;i<=n;++i){
		lev[i]=1;r[1]=i;
		tmp-=d[i][tar[i][1]];cnt=1;
		dfs(1,1);
		tmp+=d[i][tar[i][1]];
		lev[i]=0;tot=0;
	}
	printf("%d",ans);
	return 0;
}

列队

线段树动态开点,太坑了,我不会。
大意就是给每行的前m-1个点建树,另外给第m列建一棵树。模拟过程加点,查询,在查询节点时顺便减去该点,表明这个人已经离队。

因为线段树不能删点,所以要预先开足够的点。用siz数组储存这个节点包含的区间内还有多少人。在一个人离队时所有包含这个区间的节点的siz值减1,表明该区间的人数减少。一个人归队时就将其插入相应列(行)的最后。用tot数组表明该行(列)末尾归队过多少人,便于插入操作。

一个细节是,查询(query)操作时我们要找的是第几个人,而有些线段树节点是空的,因为该位置的人已经离队。查询时判断目标位置在左右子树时要与左子树的siz比较,若该子树的id未被更新过,表明前面的人都没离过队,siz即为mid-l+1。而插入(update)操作时可直接用线段树下标进行判断,因为这个人一定是站在某一行(某一列)的最后。

#include
using namespace std;

typedef long long ll;
const ll maxn=1e7;
ll n,m,q,p,ts,cur;
ll rt[maxn],siz[maxn],ls[maxn],rs[maxn],tot[maxn];
ll val[maxn];

ll getsiz(ll l,ll r){
	if(cur==n+1){
		if(r<=n) return r-l+1;
		if(l<=n) return n-l+1;
		return 0;
	}
	if(r<m) return r-l+1;
	if(l<m) return m-l;
	return 0;
} 

ll query(ll &id,ll l,ll r,ll k){
	if(!id){
		id=++ts;
		siz[id]=getsiz(l,r);
		if(l==r){
			if(cur==n+1) val[id]=l*m;
			else val[id]=(cur-1)*m+l;
		}
	}
	--siz[id];
	if(l==r) return val[id];
	ll mid=(l+r)>>1;
	if((!ls[id]&&k<=(mid-l+1))||k<=siz[ls[id]]) return query(ls[id],l,mid,k);
	else{
		if(!ls[id]) return query(rs[id],mid+1,r,k-(mid-l+1));
		else return query(rs[id],mid+1,r,k-siz[ls[id]]);
	}
}

void upd(ll &id,ll l,ll r,ll pos,ll k){
	if(!id){
		id=++ts;
		siz[id]=getsiz(l,r);
		if(l==r) val[id]=k;
	}
	++siz[id];
	if(l==r) return;
	ll mid=(l+r)>>1;
	if(pos<=mid) upd(ls[id],l,mid,pos,k);
	else upd(rs[id],mid+1,r,pos,k);
}

int main(){
	//freopen("input.txt","r",stdin);
	scanf("%lld%lld%lld",&n,&m,&q);p=max(n,m)+q;
	while(q--){
		ll x,y,z;
		scanf("%lld%lld",&x,&y);
		if(y==m){
			cur=n+1;
			z=query(rt[cur],1,p,x);
		}
		else{
			cur=x;
			z=query(rt[cur],1,p,y);
		}
		printf("%lld\n",z);
		cur=n+1;upd(rt[cur],1,p,n+(++tot[cur]),z);
		if(y!=m){
			z=query(rt[cur],1,p,x);
			cur=x;upd(rt[cur],1,p,(m-1+(++tot[cur])),z);
		}
	}
	return 0;
}

你可能感兴趣的:(Noip)