NOIP2017 题解

QAQ……由于没报上名并没能亲自去,自己切一切题聊以慰藉吧……
可能等到省选的时候我就没有能力再不看题解自己切省选题了……辣鸡HZ毁我青春

D1T1 小凯的疑惑

地球人都会做,懒得写题解了……

D1T2 时间复杂度

分类讨论+递归就行了,没啥思维含量,略。

D1T3 逛公园

这题好劲啊……
看见\(k\le 50\)应该能想到这是一个\(O((n+m)k)\)的DP,由于题目要求的是比最短路长度长至多\(k\)的路径条数,因此状态定义应该是定义\(f_{i,j}\)表示从\(i\)走到终点,长度为\(i\)到终点的最短路长度+\(j\)的路径条数(如果你习惯倒序定义的话)。
转移方程也不难写出:

\[f_{i,j}=\sum_{<i,k>,weight=w}f_{k,d_i+j-w-d_k} \]

其中\(d_i\)表示\(i\)到终点的最短路长度,边界是\(f_{n,0}=1\)
注意到这个转移方程存在同层转移的问题:
定义最短路DAG是所有点和所有满足\(d_i=d_j+w\)的边\(<i,j>,w\)组成的图(当然由于零边的存在,这个图上可能会有零环),那么转移时所有最短路DAG上的边都会有同层转移的问题。
如果有一个可以到达(总路程不超过\(D+k\))的零环那显然答案就应该是-1了。因此我们可以对最短路DAG进行拓扑排序,最后能出来的点(也就是不能沿最短路到达零环的点)一定组成一个DAG。
所以只需要判断那些出不来(在零环上或者通向零环)的点是否可达就行了。如果答案不是-1的话就只需要转移那些能出来的点,为了解决同层转移的问题,对每层先从低层转移过来,再按照最短路DAG的拓扑序同层转移即可。这样做不需要递归,常数比记忆化搜索小很多。
(注意由于数据范围比较大,最开始求\(d_i\)和构造最短路DAG时要用Dijkstra算法,不要用SPFA。)
莫名其妙的地方都能写错,老年选手身败名裂……

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int maxn=100005,maxm=200005;
struct A{
	int x,d;
	A(int x,int d):x(x),d(d){}
	bool operator<(const A &a)const{return d>a.d;}
};
void Dijkstra(int);
void bfs();
vector<int>G[maxn],W[maxn],G2[maxn],RG2[maxn];
bool vis[maxn];
int d[maxn],dis[maxn],du[maxn],q[maxn],u[maxm],v[maxm],w[maxm];
int T,n,m,k,p,f[maxn][55],head,tail;
int main(){
	scanf("%d",&T);
	while(T--){
		memset(du,0,sizeof(du));
		memset(f,0,sizeof(f));
		scanf("%d%d%d%d",&n,&m,&k,&p);
		for(int i=1;i<=n;i++){
			G[i].clear();
			W[i].clear();
			G2[i].clear();
			RG2[i].clear();
		}
		for(int i=1;i<=m;i++){
			scanf("%d%d%d",&u[i],&v[i],&w[i]);
			G[v[i]].push_back(u[i]);
			W[v[i]].push_back(w[i]);
		}
		Dijkstra(n);
		memcpy(d,dis,sizeof(d));
		for(int i=1;i<=n;i++){
			G[i].clear();
			W[i].clear();
		}
		for(int i=1;i<=m;i++){
			G[u[i]].push_back(v[i]);
			W[u[i]].push_back(w[i]);
		}
		Dijkstra(1);
		for(int i=1;i<=m;i++)if(d[u[i]]==d[v[i]]+w[i]){
			//printf("(%d,%d) w=%d\n",u[i],v[i],w[i]);
			G2[u[i]].push_back(v[i]);
			RG2[v[i]].push_back(u[i]);
			du[u[i]]++;
		}
		bfs();
		bool bad=false;
		for(int i=1;i<=n;i++)if(!vis[i]&&d[i]+dis[i]<=d[1]+k){
			bad=true;
			break;
		}
		if(bad){
			printf("-1\n");
			continue;
		}
		//for(int i=1;i<=n;i++)if(!vis[i])printf("d[%d]=%d dis[%d]=%d D=%d k=%d\n",i,d[i],i,dis[i],d[1],k);
		f[n][0]=1;
		for(int j=0;j<=k;j++){
			if(j){
				for(int x=1;x<=n;x++)if(vis[x])for(int i=0;i<(int)G[x].size();i++){
					int t=d[x]-d[G[x][i]]+j-W[x][i];//printf("%d %d t=%d\n",x,G[x][i],t);
					if(t>=0&&t<j){
						f[x][j]=f[x][j]+f[G[x][i]][t];
						if(f[x][j]>=p)f[x][j]-=p;
					}
				}
			}
			for(int i=0;i<tail;i++){
				int x=q[i];
				for(int t=0;t<(int)G2[x].size();t++){
					f[x][j]=f[x][j]+f[G2[x][t]][j];
					if(f[x][j]>=p)f[x][j]-=p;
				}
			}
			//for(int i=1;i<=n;i++)printf("f[%d][%d]=%d\n",i,j,f[i][j]);
		}
		int ans=0;
		for(int i=0;i<=k;i++){
			ans+=f[1][i];
			if(ans>=p)ans-=p;
		}
		printf("%d\n",ans);
	}
	return 0;
}
void Dijkstra(int x){
	memset(dis,63,sizeof(dis));
	memset(vis,0,sizeof(vis));
	priority_queue<A>heap;
	dis[x]=0;
	heap.push(A(x,0));
	while(!heap.empty()){
		x=heap.top().x;
		heap.pop();
		vis[x]=true;
		for(int i=0;i<(int)G[x].size();i++)if(!vis[G[x][i]]&&dis[G[x][i]]>dis[x]+W[x][i]){
			dis[G[x][i]]=dis[x]+W[x][i];
			heap.push(A(G[x][i],dis[G[x][i]]));
		}
	}
}
void bfs(){
	head=tail=0;
	for(int i=1;i<=n;i++)if(!du[i])q[tail++]=i;
	memset(vis,0,sizeof(vis));
	while(head!=tail){
		int x=q[head++];//printf("%d ",x);
		vis[x]=true;
		for(int i=0;i<(int)RG2[x].size();i++)if(!--du[RG2[x][i]])q[tail++]=RG2[x][i];
	}
	//printf("\n");
}

D2T1 奶酪

\(O(n^2)\)暴力就行了,水题。

D2T2 宝藏

看到数据范围一眼\(O^*(3^n)\)状压DP,其中\(3^n\)来自枚举子集的子集。做法好像有很多,比如ryf的做法就比我快了10倍……日渐辣鸡的窝
定义\(f_{i,j,S}\)表示以\(i\)为根的子树,\(i\)在整棵树里的深度是\(j\),集合\(S\)中的所有点都在这棵子树中时这棵子树的最小总代价。
不难写出转移方程:

\[f_{i,j,S}=\min_{T\subsetneq S,k\in T}\{f_{k,j+1,T}+f_{i,j,S-T}+j\times w_{i,k}\} \]

(这个转移方程是在枚举与\(i\)相邻的一个点\(k\)并把以\(k\)为根的子树分出去)
然而你发现这个DP是\(O(n^3 3^n)\)的,并不能跑过去。
考虑优化转移,如果把转移方程中与\(S\)无关的部分拿出来并定义一个辅助数组

\[g_{i,j,T}=\min_{k\in T}\{f_{k,j+1,T}+j\times w_{i,k}\} \]

的话,我们就可以把状态转移方程改写成

\[f_{i,j,S}=\min_{T\subsetneq S}\{f_{i,j,S-T}+g_{i,j,T}\} \]

这样就可以把复杂度降到\(O(n^2 3^n)\)了,由于常数不大,并不需要卡常。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define bit(x) (1<<((x)-1))
using namespace std;
const int INF=0x3f3f3f3f;
int n,m,w[15][15],f[15][15][(1<<12)+1],g[15][15][(1<<12)+1];
int main(){
	scanf("%d%d",&n,&m);
	memset(w,63,sizeof(w));
	memset(f,63,sizeof(f));
	memset(g,63,sizeof(g));
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		w[x][y]=w[y][x]=min(w[x][y],z);
	}
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)f[i][j][bit(i)]=0;
	for(int j=n-1;j;j--)for(int s=0;s<(1<<n);s++)for(int i=1;i<=n;i++){
		for(int k=1;k<=n;k++)if((bit(k)|s)==s&&w[i][k]<INF)
			g[i][j][s]=min(g[i][j][s],f[k][j+1][s]+w[i][k]*j);
		for(int t=s&(s-1);;(--t)&=s){
			f[i][j][s]=min(f[i][j][s],f[i][j][s^t]+g[i][j][t]);
			if(!t)break;
		}
	}
	int ans=INF;
	for(int i=1;i<=n;i++)ans=min(ans,f[i][1][(1<<n)-1]);
	printf("%d",ans);
	return 0;
}

D2T3 列队

这题真是劲啊……比往年的数据结构NB到不知哪儿去了……
(码力太差还没写出来,写出来之后再补题解)

posted @ 2017-11-18 18:30  AntiLeaf  阅读(1210)  评论(0编辑  收藏  举报