NOIP2017提高组题解

D1T1小凯的疑惑\((OK)\)

D1T2时间复杂度

D1T3逛公园\((OK)\)

D2T1奶酪\((OK)\)

D2T2宝藏\((OK)\)

D2T3列队

话说我真的不是故意每一年都只做\(4\)道的,而是每年剩下两道都不可做...其实时间复杂度不难,但因为是字符串\(+\)大模拟就咕咕咕了.

\(D1T1\)今天重新写的时候早忘记结论了,就想说自己推,然后利用类似于完全背包的东西随便打了个表,十几分钟就发现了规律.

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define ll long long
using namespace std;
inline int read(){
    int x=0,o=1;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')o=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
    return x*o;
}
//int f[10005];
int main(){
	/*for(int a=2;a<=20;++a){
		for(int b=2;b<=20;++b){
			cout<<a<<" "<<b<<":";
			memset(f,0,sizeof(f));f[0]=1;
			for(int i=a;i<=10000;++i)f[i]|=f[i-a];
			for(int i=b;i<=10000;++i)f[i]|=f[i-b];
			for(int i=10000;i>=2;--i)if(!f[i]){cout<<i<<endl;break;}
		}
	}*/
	ll a=read(),b=read();
	printf("%lld\n",1ll*(b-2)*a+(a-b));
    return 0;
}

\(D1T2\)字符串+大模拟=咕咕咕.

\(D1T3\)做了半个上午,每次碰到这种图论题一下正图一下反图的,脑袋就不好使了(可能是一直都不太好使吧).

大概讲一下,就是先建个反图,从终点\(n\)开始跑最短路,记\(dis[i]\)表示节点\(i\)到终点\(n\)的最短路的长度.然后建正图,设\(f[i][j]\)表示从节点\(i\)到终点\(n\),当前路径长度还能够比最短路长\(j\)(即你还可以比最短路多走j的长度)的路径方案数.所以最终答案就是\(f[1][k]\).

然后考虑怎么求这个东西,\(DP\)或者记忆化搜索.记忆化搜索虽然本质上就是\(DP\),但是代码实现起来比\(DP\)简单多了.考虑当前在节点\(u\),有一条边\((u,v,w)\),那么\(f[u][k]+=f[v][k-(dis[v]+w-dis[u])]\),很好理解,因为在节点\(u\)时还剩下多余的\(k\)步可走,那么从\(u->v\)我们浪费了\(dis[v]+w-dis[u]\),所以在节点\(v\)时就只剩下\(k-(dis[v]+w-dis[u])\)多余的步了.

然后还要考虑判断无穷多解的情况,无穷多解即有一条合法路径上出现了\(0\)环,那么在记搜的同时再开一个数组\(g\)记录当前这个状态\(g[i][j]\)有没有访问过,如果这个状态之前已经访问过了,说明出现了\(0\)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define ll long long
using namespace std;
inline int read(){
    int x=0,o=1;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')o=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
    return x*o;
}
const int N=100005;
const int M=200005;
int n,m,K,p,a[M],b[M],c[M];
int tot,head[N],nxt[M],to[M],w[M];
int dis[N],visit[N],f[N][51],g[N][51];
inline void add(int a,int b,int c){
	nxt[++tot]=head[a];head[a]=tot;
	to[tot]=b;w[tot]=c;
}
inline void dij(){
	memset(visit,0,sizeof(visit));
	memset(dis,0x3f,sizeof(dis));
	priority_queue<pair<int,int> >q;
	q.push(make_pair(0,n));dis[n]=0;
	while(q.size()){
		int u=q.top().second;q.pop();
		if(visit[u])continue;visit[u]=1;
		for(int i=head[u];i;i=nxt[i]){
			int v=to[i];
			if(dis[v]>dis[u]+w[i]){
				dis[v]=dis[u]+w[i];
				q.push(make_pair(-dis[v],v));
			}
		}
	}
}
inline int dfs(int u,int k){//记忆化搜索
	if(g[u][k])return -1;//这个状态之前访问过=出现了0环
	if(f[u][k])return f[u][k];//之前搜过这个状态,就不再搜了
	g[u][k]=1;//标记该状态访问过
    f[u][k]=(u==n);//初始化
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i],dist=dis[v]+w[i]-dis[u];
		if(k-dist>=0){
			int cnt=dfs(v,k-dist);
			if(cnt==-1)return f[u][k]=-1;
			else f[u][k]=(f[u][k]+cnt)%p;
		}
	}
	g[u][k]=0;//回溯
    return f[u][k];
}
int main(){
	int T=read();
	while(T--){
		n=read();m=read();K=read();p=read();
		tot=0;memset(head,0,sizeof(head));
		for(int i=1;i<=m;++i){
			a[i]=read();b[i]=read();c[i]=read();
			add(b[i],a[i],c[i]);
		}
		dij();//反向图最短路
		tot=0;memset(head,0,sizeof(head));//初始化建正图
		for(int i=1;i<=m;++i)add(a[i],b[i],c[i]);
		memset(f,0,sizeof(f));memset(g,0,sizeof(g));
		printf("%d\n",dfs(1,K));
	}
    return 0;
}

\(D2T1\)方法好像挺多的,我觉得我能够一下想到的,应该也是最容易想到的方法吧.就是\(BFS\),初始时把每个能从\(z=0\)钻入的洞丢进队列,每次暴力枚举扩展相连通的洞,找到一个能够到达\(z=h\)的洞即可.

然后我自己\(WA\)了几次是因为几何数学没有学好???对于一个洞的球心\((x,y,z)\),如果它要能够到达\(z=0/h\),那么只要\(dist((x,y,z),(0,0,0/h))<=r\)即可,而我们\(BFS\)扩展的时候,对于两个洞的球心\((x1,y1,z1),(x2,y2,z2)\),这两个洞要连通只要保证\(dist<=2*r\).(之前没考虑好,全都是写的小于等于\(2r\),竟然还有\(70\)分)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define ll long long
using namespace std;
inline int read(){
    int x=0,o=1;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')o=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
    return x*o;
}
const int N=1005;
int n,h,r,visit[N];
struct dong{int x,y,z;}a[N];
inline double dis(int i,int j){
	return (double)sqrt(1.0*(a[i].x-a[j].x)*(a[i].x-a[j].x)+1.0*(a[i].y-a[j].y)*(a[i].y-a[j].y)+1.0*(a[i].z-a[j].z)*(a[i].z-a[j].z));
}
int main(){
	int T=read();
	while(T--){
		memset(visit,0,sizeof(visit));queue<int>q;
		n=read();h=read();r=read();a[0].x=0;a[0].y=0;a[0].z=0;
		for(int i=1;i<=n;++i){
			a[i].x=read();a[i].y=read();a[i].z=read();
			if((double)sqrt(1.0*a[i].z*a[i].z)<=(double)r)q.push(i),visit[i]=1;
		}
		a[n+1].x=0;a[n+1].y=0;a[n+1].z=h;int bj=0;
		while(q.size()){
			int u=q.front();q.pop();
			if((double)sqrt(1.0*(a[u].z-h)*(a[u].z-h))<=(double)r){bj=1;break;}
			for(int i=1;i<=n;++i){
				if(visit[i])continue;
				if(dis(u,i)<=(double)2.0*r){q.push(i);visit[i]=1;}
			}
		}
		if(bj)puts("Yes");
		else puts("No");
	}
    return 0;
}

\(D2T2\) \(n<=12\)很明显的状压,然后刚开始状态设错,搞了半个上午都没做出来.只好去看题解了

因为\(DP\)扩展状态的时候 会发现贡献与当前这个节点距离根节点(最开始选的那一个宝藏屋)有关,所以考虑把这个设入状态.

\(f[i][j]\)表示当前考虑好了的点集是\(j\),扩展到了第\(i\)层(即点集\(j\)中距离根节点最深的节点的深度是\(i\))是的最小花费.

\(f[i][j|k]=min(f[i][j|k],f[i-1][j]+dist[j][k]*(i-1))\).\(j,k\)是两个互不相交的集合,\(dist[j][k]\)表示使得\(j,k\)这两个集合连通的最短距离.

我们可以先预处理\(dis[i][j]\)表示点\(i\)到点集\(j\)的最短距离(点\(i\)与点集\(j\)中的点直接连通).然后再通过\(dis[i][j]\)求出\(dist[i][j]\).注意到我们需要枚举与一个集合互不相交的所有集合,其实就是该集合的补集的所有子集.

我为了卡常,把所有的\(memset\)改成了循环,所以代码可能很丑陋.为什么最近做的几道\(NOIP\)题都需要卡常啊,本题卡常前总用时\(5.45s\),\(T\)\(4\)个点,卡常后总用时\(3.53s\),\(AC\).区别还是挺大的啊.

本题因为\(NOIP\)的数据水,还有各种奇怪又美妙的做法,时间复杂度能够吊打状压.

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define rg register
#define ll long long
using namespace std;
inline int read(){
    rg int x=0,o=1;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')o=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*o;
}
const int inf=2e9;
int w[15][15],dis[15][1<<12],dist[1<<12][1<<12],f[15][1<<12];
int main(){
	rg int n=read(),m=read();
	for(rg int i=1;i<=n;++i)
		for(rg int j=1;j<=n;++j)w[i][j]=inf;
	for(rg int i=1;i<=m;++i){
		rg int a=read(),b=read(),c=read();
		if(c<w[a][b])w[a][b]=c,w[b][a]=c;//显然有重边
	}
	rg int S=(1<<n)-1;
	for(rg int i=1;i<=n;++i)
		for(rg int j=1;j<=S;++j)dis[i][j]=inf;
	for(rg int i=1;i<=n;++i){//预处理dis数组
		for(rg int j=1;j<=S;++j){
			if(!(j&(1<<(i-1)))){
				for(rg int k=1;k<=n;++k)
					if(j&(1<<(k-1)))dis[i][j]=min(dis[i][j],w[k][i]);
			}
		}
	}
	for(rg int i=1;i<=S;++i){//预处理dist[i][j]
		for(rg int j=i^S;j;j=(j-1)&(i^S)){//枚举子集
			for(rg int k=1;k<=n;++k){
				if((j&(1<<(k-1)))){
					if(dis[k][i]==inf){dist[i][j]=0;break;}
//只要集合j中有一个点无法到达集合$i$,就说明这两个集合不能连通
//因为这里的连通都是建立在直接相连的意义下的,这样才能保证后面一层一层更新的正确性
					dist[i][j]+=dis[k][i];
				}
			}
		}
	}
	rg int ans=2e9;
	for(rg int st=1;st<=n;++st){//枚举根节点
		for(rg int i=1;i<=n;++i)
			for(rg int j=0;j<=S;++j)f[i][j]=inf;
		f[1][1<<(st-1)]=0;//初始化
		for(rg int i=2;i<=n;++i){
			for(rg int j=1;j<=S;++j){
				for(rg int k=j^S;k;k=(k-1)&(j^S)){
					if(dist[j][k])f[i][j|k]=min(f[i][j|k],f[i-1][j]+dist[j][k]*(i-1));
				}
			}
		}
		for(rg int i=1;i<=n;++i)if(f[i][S]<ans)ans=f[i][S];
	}
	printf("%d\n",ans);
    return 0;
}

posted on 2019-10-30 16:13  PPXppx  阅读(359)  评论(0编辑  收藏  举报