(转载于我的洛谷博客)
索引:
第一题: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分钟的空间时的最大录制歌曲数。那么对于每一首歌一共有三种状态:
- 不选这首歌
- 在当前光盘里存这首歌
- 在一张新光盘里存这首歌
故状态转移方程为:\(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写起来比较麻烦,所以我们可以通过其他的方式来解这个题,比如随机化贪心
我们知道,贪心每次求出的都是较优解,所以我们可以通过打乱木板数组的顺序来进行贪心,记录每一次的较优解,最优解很可能就在其中了
以下是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;
}