题解小合集——第八弹

(转载于我的洛谷博客

索引:

第一题:P2564 生日礼物

第二题:P3084 照片

第三题:P4878 布局

第四题:P2736 “破锣摇滚”乐队

第五题:P4568 飞行路线

第六题:P1284 三角形牧场

第七题:P3959 宝藏

第八题:P1197 星球大战

第九题:P1337 平衡点

第十题:P1325 雷达安装

第一题:P2564 生日礼物

题解思路:单调队列(尺取法)

这题和P1638 逛画展很像,唯一的区别就是可能同一个位置上可能都有多个"彩珠"

然而有多个"彩珠"这点有和P2698 花盆这题一样

所以我们可以继承两题的思路,用P1638的单调队列和P2698里的结构体来解决这个题。显然,我们的结构体要记录两个数据:位置,种类。然后我们对位置排序,以便于维护单调序列。

我们用一个指针l指向区间的最左端的珠子,然后遍历所有珠子(不用去循环右端点,因为在彩带上会有地方没有珠子,把右端点放在没有珠子的地方是没有意义的),每增加一颗i种类的珠子,kind[i]就记录i种类的珠子的最近出现的位置。如果又找到了一颗和 区间最左端的珠子 相同的珠子,那么最左端的珠子就没有存在的必要了,把它出队即可。当区间包含所有种类的珠子时,我们更新并记录最优答案,在遍历完之后输出即可。

请结合代码加深理解……

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
struct Node
{
	int x,y;
}a[1000010];
int n,m,ans=0x3f3f3f3f,cnt,k[65];
bool cmp(const Node &a,const Node &b)
{
	return a.x<b.x;
}
int main()
{
	memset(k,-1,sizeof(k));
	scanf("%d%d",&n,&m);
	int b,c;
	for(int i=1;i<=m;i++)
	{
		scanf("%d",&b);
		for(int j=1;j<=b;j++)
		{
			scanf("%d",&c);
			a[++cnt].x=c;
			a[cnt].y=i;
		}
	}
	sort(a+1,a+n+1,cmp);
	int l=1;cnt=0;
	for(int i=1;i<=n;i++)
	{
		if(k[a[i].y]==-1)cnt++;
		k[a[i].y]=a[i].x;
		while(l<=i&&a[l].x!=k[a[l].y])l++;
		if(cnt==m&&a[i].x-a[l].x<ans)ans=a[i].x-a[l].x;
	}
	printf("%d",ans);
	return 0;
}

第二题:P3084 照片

题解思路:差分约束系统+最短路

我觉得这个题和Intervals有相似之处(题解小合集——第六弹第六题)

我们在利用差分约束系统解题的过程中一定要关注题目的设问,若求的是两个变量差的最大值,那么将所有不等式转变成"<="的形式并且在建图后求最短路;反之则转换成">="的形式,并且求最长路

显然,本题问的是“最多可能有多少只斑点奶牛”,所以我们一定要把不等式都化成\(a-b<=c\)的形式,然后建边\((b,a)=c\),然后跑最短路。

不过,USACO的题日常卡spfa,只是这样做是过不了了,我们要用双端队列优化spfa

我们在原来的spfa中会这样:

if(!vis[v])
{
	vis[v]=1;
    q.push(v);
}

双端队列优化后就成了这样:

if(q.size()&&dis[q.front()]<dis[v])q.push_back(v);
else q.push_front(v);

意义很显然,当队首元素大于等于当前节点的dis,我们就把他放入队首,下次先扩展

但是加了这个优化以后才90分,这又是咋回事呢?

我们记录所有节点的总入队次数,如果过大就直接return

最后别忘了判环

以下是AC代码:

#include<deque>
#include<cstdio>
#include<iostream>
#define reg register
using namespace std;
struct Edge
{
	int nst,to,dis;
}edge[800010],ddd;
int head[400010],dis[400010],cnt,n,m,num[400010],tot;
bool vis[400010];
inline void add(int a,int b,int c)
{
	edge[++cnt].nst=head[a];
	edge[cnt].to=b;
	edge[cnt].dis=c;
	head[a]=cnt;
}
inline bool spfa()
{
	deque <int> q;
	for(int i=1;i<=n;i++)
	{
		dis[i]=0x3f3f3f3f;
		vis[i]=0;
	}
	dis[0]=0;
	vis[0]=1;
	q.push_back(0);
	while(!q.empty())
	{
		int u=q.front();
		q.pop_front();
		vis[u]=0;
		num[u]++;
		if(num[u]>=n)return 1;
		for(int i=head[u];i;i=edge[i].nst)
		{
			int v=edge[i].to;
			if(dis[v]>dis[u]+edge[i].dis)
			{
				dis[v]=dis[u]+edge[i].dis;
				if(!vis[v])
				{
					tot++;
					
					if(q.size()&&dis[q.front()]<dis[v])q.push_back(v);
					else q.push_front(v);
					vis[v]=1;
				}
				if(tot>2003212)return 1;
			}
		}
	}
	return 0;
}
int main()
{
	scanf("%d%d",&n,&m);
	int a,b;
	for(reg int i=1;i<=m;i++)
	{
		scanf("%d%d",&a,&b);
		add(a-1,b,1);
		add(b,a-1,-1);
	}
	for(reg int i=1;i<=n;i++)
	add(i-1,i,1),add(i,i-1,0);
	if(spfa()){printf("-1");return 0;}
	printf("%d",dis[n]);
	return 0;
}

第三题:P4878 布局

题解思路:差分约束

这个题和上面的第二题很像,但是有一点不同——上面的题不需要超级源点,因为它所有的点都是相互连通的(每两个点之间建了双向边),而且建图时需要把左端点减一,因为这样\(f[b]-f[a-1]\)才是区间\([a,b]\)上斑点奶牛的数量。

但是这个题却不用,因为这个题里的\(f[i]\)表示的是从奶牛1到奶牛i的距离,所以\(f[i]-f[1]\)即为1-i的距离。而且,由于此题中两点间只建了单向边,所以无法确保图的连通性,所以需要0作为超级源点来判断连通性。故此题的思路是先跑一遍spfa(0)判连通再跑一遍spfa(1)求距离

以下是AC代码:

#include<deque>
#include<cstdio>
#include<iostream>
#define reg register
using namespace std;
struct Edge
{
	int nst,to,dis;
}edge[800010],ddd;
int head[400010],dis[400010],cnt,n,m,k,num[400010],tot;
bool vis[400010];
inline void add(int a,int b,int c)
{
	edge[++cnt].nst=head[a];
	edge[cnt].to=b;
	edge[cnt].dis=c;
	head[a]=cnt;
}
inline bool spfa(int s)
{
	deque <int> q;
	for(int i=1;i<=n;i++)
	{
		dis[i]=0x3f3f3f3f;
		vis[i]=0;
	}
	dis[s]=0;
	vis[s]=1;
	q.push_back(s);
	while(!q.empty())
	{
		int u=q.front();
		q.pop_front();
		vis[u]=0;
		num[u]++;
		if(num[u]>=n)return 1;
		for(int i=head[u];i;i=edge[i].nst)
		{
			int v=edge[i].to;
			if(dis[v]>dis[u]+edge[i].dis)
			{
				dis[v]=dis[u]+edge[i].dis;
				if(!vis[v])
				{
					tot++;
					
					if(q.size()&&dis[q.front()]<dis[v])q.push_back(v);
					else q.push_front(v);
					vis[v]=1;
				}
				if(tot>2003212)return 1;
			}
		}
	}
	return 0;
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	int a,b,c;
	for(reg int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);
	}
	for(reg int i=1;i<=k;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		add(b,a,-c);
	}
	for(reg int i=1;i<=n;i++)
	add(i+1,i,0);
	for(reg int i=1;i<=n;i++)
	add(0,i,0);
	if(spfa(0)){printf("-1");return 0;}
	spfa(1);
	if(dis[n]>=0x3f3f3f3f){printf("-2");return 0;}
	else printf("%d",dis[n]);
	return 0;
}

第四题:P2736 “破锣摇滚”乐队

题解思路:二维费用的背包问题

关于二维费用的背包问题详见背包九讲

咳咳,对于这个题,也不是裸的二维费用板子,可以进行优化和改进的。我们用\(f[i][j]\)表示目前用了i张光盘,最后一张光盘还剩j分钟的空间时的最大录制歌曲数。那么对于每一首歌一共有三种状态:

  1. 不选这首歌
  2. 在当前光盘里存这首歌
  3. 在一张新光盘里存这首歌

故状态转移方程为:\(f[i][j]=max(f[i][j],f[i-1][t]+1,f[i][j-a[k]]+1)\),k是歌曲序号

还有一点需要注意,我们把光盘和歌曲时长看成二维费用,由于是01背包的变形,所以循环这两个变量时要按倒序

以下是AC代码:

#include<cstdio>
#include<iostream>
using namespace std;
int n,m,t,a[25];
int f[25][25];
int maxx(int a,int b,int c)
{
	a=max(a,b);
	a=max(a,c);
	return a;
}
int main()
{
	scanf("%d%d%d",&n,&t,&m);
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
		for(int j=m;j>=1;j--)
			for(int k=t;k>=a[i];k--)
			f[j][k]=maxx(f[j][k],f[j-1][t]+1,f[j][k-a[i]]+1);
	printf("%d",f[m][t]);
	return 0;
}

第五题:P4568 飞行路线

题解思路:多层图

这个是多层图板子题……

我们在各层内部正常连边,各层之间从上到下连权值为0的边。每向下跑一层,就相当于免费搭一次飞机。

当然,由于某些毒瘤出题人会设置一些不用坐k次免费飞机就能过的数据,所以我们在每层图的终点之间连边权为0的边,最后跑\(s\)->\(t+k*n\)的最短路就好了

以下是AC代码:

#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
struct Edge
{
	int nst,to,dis;
}edge[2000010];
int head[110010],dis[110010],n,m,k,cnt,s,t;
bool vis[110010];
inline int read()
{
	int fu=1,x=0;char o=getchar();
	while(o<'0'||o>'9'){if(o=='-')fu=-1;o=getchar();}
	while(o>='0'&&o<='9'){x=(x<<1)+(x<<3)+(o^48);o=getchar();}
	return x*fu;
}
inline void add(int a,int b,int c)
{
	edge[++cnt].nst=head[a];
	edge[cnt].to=b;
	edge[cnt].dis=c;
	head[a]=cnt;
}
void dijkstra()
{
	priority_queue <pair<int,int> > q;
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	q.push(make_pair(0,s));
	while(q.size())
	{
		int u=q.top().second;
		q.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(int i=head[u];i;i=edge[i].nst)
		{
			int v=edge[i].to;
			if(dis[v]>dis[u]+edge[i].dis)
			{
				dis[v]=dis[u]+edge[i].dis;
				q.push(make_pair(-dis[v],v));
			}
		}
	}
}
int main()
{
	n=read();m=read();k=read();
	int a,b,c;
	s=read();t=read();
	for(int i=1;i<=m;i++)
	{
		a=read();b=read();c=read();
		add(a,b,c);
		add(b,a,c);
		for(int j=1;j<=k;j++)
		{
			add(a+(j-1)*n,b+j*n,0);
			add(b+(j-1)*n,a+j*n,0);
			add(a+j*n,b+j*n,c);
			add(b+j*n,a+j*n,c);
		}
	}
	for(int i=1;i<=k;i++)
	add(t+(i-1)*n,t+i*n,0);
	dijkstra();
	printf("%d",dis[t+k*n]);
	return 0;
}

第六题:P1284 三角形牧场

题解思路:DP(正解)或随机化贪心(骗分)

DP正解

这个题的DP写起来比较麻烦,所以我们可以通过其他的方式来解这个题,比如随机化贪心

我们知道,贪心每次求出的都是较优解,所以我们可以通过打乱木板数组的顺序来进行贪心,记录每一次的较优解,最优解很可能就在其中了

以下是AC代码:

#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define re register
using namespace std;
int n,x[45],a[3];
int ans=-1;
int clac(double a,double b,double c)
{
	if(a+b>c&&a+c>b&&b+c>a)
	{
		double p=(a+b+c)/2;
		return trunc(sqrt(p*(p-a)*(p-b)*(p-c))*100);
	}
	else return -1;
}
void work()
{
	a[0]=x[1],a[1]=x[2],a[2]=x[3];
	for(int i=4;i<=n;i++)
	{
		int tmp=0x3f3f3f3f,id;
		for(int j=0;j<=2;j++)if(tmp>a[j])tmp=a[j],id=j;
		a[id]+=x[i];
	}
	ans=max(ans,clac(a[0],a[1],a[2]));
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	scanf("%d",&x[i]);
	for(int i=1;i<=500000;i++)
	{
		random_shuffle(x+1,x+1+n);
		work();
	}
	printf("%d",ans);
	return 0;
}

第七题:P3959 宝藏

题解思路:DFS(正解)或模拟退火

正解在这里

我的DFS从40分改到了70分,但巨佬们说的玄学剪枝我看不懂,所以AC难度就比较大

顺便一说,把DFS从40分改到70分只需一句话:

dis[i][j]=dis[j][i]=min(dis[i][j],c);

因为“对于70%的数据,\(1≤n≤8,0≤m≤1000,v≤5000\)”,显然整个图内最多有64条边,m却有1000条,所以一定有重边,我们每次选择重边中最小的一条就好了

这是我的70分代码:

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int inf=0x3f3f3f3f;
int e[20][20],n,m,dis[20][20],tmp,l[20],ans=inf;
inline void dfs(int e,int num)
{
	if(n==num)
	{		
		ans=min(ans,tmp);
		return;
	}
	if(tmp>=ans)return;
	for(int i=1;i<=n;i++)
	{
		if(l[i])continue;
		for(int j=1;j<=n;j++)
		{
			if(dis[j][i]==inf||!l[j]||i==j)continue;
			tmp+=l[j]*dis[j][i];
			l[i]=l[j]+1;
			dfs(i,num+1);
			tmp-=l[j]*dis[j][i];
			l[i]=0;
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	int a,b,c;
	memset(dis,63,sizeof(dis));
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		dis[a][b]=dis[b][a]=min(dis[a][b],c);
	}
	for(int i=1;i<=n;i++)
	{
		l[i]=1;dfs(i,1);l[i]=0;
	}
	printf("%d",ans);
	return 0;
}

接下来说模拟退火(其实严格来讲并不是模拟退火,而是随机化prim)

虽然这个题prim已经被证明了是不正确的,但是我们可以通过随机化,让prim有一定概率去找距离更远的点,就有可能找到最优解。而且该算法的复杂度并不高,我们跑1000遍随机化prim,总有一次能找到最优解(毕竟n这么小)

以下是 看脸 AC代码:

#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;
int n,m,e[13][13],dep[13];
const int inf=0x3f3f3f3f;
struct Edge
{
	int u,v;
};
bool operator < (struct Edge a,struct Edge b)
{
	return dep[a.u]*e[a.u][a.v]>dep[b.u]*e[b.u][b.v];
}
int find(int s)
{
	memset(dep,0,sizeof(dep));
	int vis[13]={0};
	priority_queue <Edge> heap;
	Edge past[1000];
	int p=0;
	Edge a,b;
	int cost=0;
	dep[s]=1;
	vis[s]=1;
	for(int i=1;i<=n;i++)
	if(e[s][i]<inf)
	{
		a.u=s;
		a.v=i;
		heap.push(a);
	}
	for(int i=1;i<n;i++)
	{
		a=heap.top();heap.pop();
		while(!heap.empty()&&((vis[a.v]||rand()%(n)<1)))
		{
			if(!vis[a.v])past[p++]=a;
			a=heap.top();
			heap.pop();
		}
		vis[a.v]=1;
		dep[a.v]=dep[a.u]+1;
		if(p-->0)
		{
			for(;p>=0;p--)heap.push(past[p]);
		}
		p=0;
		for(int i=1;i<=n;i++)
		if(e[a.v][i]<inf&&!vis[i])
		{
			b.u=a.v;b.v=i;
			heap.push(b);
		}
		cost+=e[a.u][a.v]*dep[a.u];
	}
	return cost;
}
int main()
{
	scanf("%d%d",&n,&m);
	int a,b,c;
	memset(e,63,sizeof(e));
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		e[a][b]=e[b][a]=min(e[a][b],c);
	}
	srand(19260817);
	int minn=inf;
	for(int j=1;j<1000;j++)
	for(int i=1;i<=n;i++)
	minn=min(minn,find(i));
	printf("%d",minn);
	return 0;
}

第八题:P1197 星球大战

题解思路:并查集

我们发现,对于这个题,要把一个点从图中割去真是难了去了,但是如果是向图中添加点却容易地多。所以我们用并查集判断联通性,然后逐个加点就好了。

在每次加点的时候,首先那个点会先成为一个新的连通块,然后每合并一次并查集,连通块数量就减少一个。注意,一开始我们要初始化连通块数量。

以下是AC代码:

#include<cstdio>
#include<iostream>
using namespace std;
struct Edge
{
	int nst,to;
}edge[500010];
int head[500010],cnt,f[500010],c[500010],n,m,ans[500010],k,t[500010];
void add(int a,int b)
{
	edge[++cnt].nst=head[a];
	edge[cnt].to=b;
	head[a]=cnt;
}
int find(int a)
{
	return f[a]==a?a:f[a]=find(f[a]);
}
int main()
{
	scanf("%d%d",&n,&m);
	int a,b;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}
	scanf("%d",&k);
	
	for(int i=0;i<k;i++)
	{
		scanf("%d",&c[i]);
		t[c[i]]=1;
	}
	ans[k]=n-k;
	
	for(int i=0;i<n;i++)//并查集初始化
	f[i]=i;
	
	for(int i=0;i<n;i++)//初始的连通块数量
	if(!t[i])//如果起点
	{
		for(int j=head[i];j;j=edge[j].nst)
		{
			int v=edge[j].to;
			if(!t[v])//和终点都没被摧毁
			{
				a=find(v);b=find(i);
				if(a!=b)f[a]=b,ans[k]--;//就合并一次,减少一个连通块数量
			}
		}
	}
	
	for(int i=k-1;i>=0;i--)//倒序逐个加点
	{
		ans[i]=ans[i+1]+1;t[c[i]]=0;
		for(int j=head[c[i]];j;j=edge[j].nst)
		{
			int v=edge[j].to;
			if(!t[v])//还没加入的点不能取
			{
				a=find(v);b=find(c[i]);
				if(a!=b)f[a]=b,ans[i]--;
			}
		}
	}
	
	for(int i=0;i<=k;i++)
	printf("%d\n",ans[i]);
	return 0;
 } 

第九题:P1337 平衡点

题解思路:模拟退火(需要看脸

由于电脑问题,目前写博客比较困难,先贴代码:

#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define re register
using namespace std;
struct node { int x,y,w;};
node a[1010];
int n,sx,sy;
double ansx,ansy; //全局最优解的坐标
double ans=1e18,t;//全局最优解、温度
const double delta=0.993;//降温系数
inline double calc_energy(double x,double y) //计算整个系统的能量
{ 
    double rt=0;
    for(re int i=1;i<=n;i++) 
	{
        double deltax=x-a[i].x,deltay=y-a[i].y;
        rt+=sqrt(deltax*deltax+deltay*deltay)*a[i].w;
    }
    return rt;
}
inline void simulate_anneal() //SA主过程
{
    double x=ansx,y=ansy;
    t=2000; //初始温度
    while (t>1e-14) 
	{
        double X=x+((rand()<<1)-RAND_MAX)*t;
        double Y=y+((rand()<<1)-RAND_MAX)*t;//得出一个新的坐标
        double now=calc_energy(X,Y);
        double Delta=now-ans;
        if (Delta<0) //接受
		{
            x=X,y=Y;
            ansx=x,ansy=y,ans=now;
        }
        else if (exp(-Delta/t)*RAND_MAX>rand()) x=X,y=Y;//以一个概率接受
        t*=delta;
    }
}

inline void Solve() //多跑几遍SA,减小误差
{ 
    ansx=(double)sx/n,ansy=(double)sy/n; //从平均值开始更容易接近最优解
    simulate_anneal();
    simulate_anneal();
    simulate_anneal();
}

int main() 
{
    srand(19260817);
	srand(rand());//玄学srand
	srand(rand());
    scanf("%d",&n);
    for(re int i=1;i<=n;i++)
	{
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].w);
        sx+=a[i].x,sy+=a[i].y;
    }
    Solve();
    printf("%.3f %.3f\n",ansx,ansy);
    return 0;
}

第十题:P1325 雷达安装

题解思路:排序+贪心

由于雷达站只能建在海岸上,所以很容易想到这是一个区间覆盖的题(@P1514 引水入城)。所以说,我们先把每个岛屿对应的区间处理出来,再按照右端点进行升序排序(如果右端点一样就按照左端点降序排序),如果下一个区间的左端点的坐标比当前区间的右端点小,就ans++,最后输出答案

以下是AC代码:

#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
struct Node
{
	double l,r;
}a[1010];
int n,d,x,y,cnt,ans;
double cur=-0x3f3f3f3f;
double calc(int a)
{
	return sqrt(d*d-a*a);
}
bool cmp (const Node &x,const Node &y)
{
	if(x.r!=y.r)return x.r<y.r;
	return x.l>y.l;
}
int main()
{
	scanf("%d%d",&n,&d);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&x,&y);
		double c=calc(y);
		a[++cnt].l=x-c;
		a[cnt].r=x+c;
	}
	sort(a+1,a+1+n,cmp);
	for(int i=1;i<=n;i++)
	{
		if(cur<a[i].l)
		{
			cur=a[i].r;
			ans++;
		}
		else continue;
	}
	printf("%d",ans);
	return 0;
} 
posted @ 2019-08-13 11:06  ETO组织成员  阅读(180)  评论(0)    收藏  举报
Live2D