Test 2022.09.21
今天是\(JSOI2010\)专场
T1满汉全席(2-SAT)
比较生疏的知识点,当时看着一眼就觉得是图论,甚至想到了最大流,但是因为建不来图,被迫去打了暴力,然而只得到\(10pts\),事实证明我的想法是正确的,但是太菜了。
关于\(2-SAT\)
这类问题就是对于\(n\)个不同的命题,存在命题\(a,b\),若\(a\)不成立,那么\(b\)就必须成立(反之亦然)。
举个例子
有三对夫妻\(ai,bi\)要参加舞会,每对夫妻至少有一人参加,是否存在一种方案使得题意被满足。建立状态\(0,1\)表示参加和不参加,已知有一些夫妻之间存在矛盾,例如\(a1,b2\)不能同时存在,即\(a1\)参加(\(1\))\(b2\)就不能参加(\(0\)),就可以用建图和\(tarjan\)求强连通分量、或者是\(dfs\)来解决建图后的问题。
根据以上性质,在这个例子中我们可以从\(a1\)的\(1\)状态向\(b2\)的\(0\)状态连一条有向边,再从\(a1\)的\(0\)状态向\(b2\)的\(1\)状态连另一条有向边,这样我们就成功地描述了事情之间的因果逻辑。
解决问题
在建好图以后,怎么处理这样一条条的逻辑关系呢?我们可以使用\(tarjan\)求强联通分量的算法,为什么呢?因为这些逻辑关系实际上就是单向的“导致”,在一个强连通分量内,所有命题必须同时存在,如果有两个互斥的条件同时存在于一个\(SCC\)中,那么就是不能满足的。
同理在这个题里,如果一个评委的要求\(1\)不能被满足,那么要求\(2\)就是必须被满足的,所以我们建边的方式就有了,之后的\(tarjan\)照着打就行了,只是多组数据记得该重置的要重置(特别是head[maxn]数组!!)
点击查看代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e4+10;
int n,m;
struct Edge{int u,v;int nex;}E[maxn];
int tote=0;int head[maxn];
void add(int u,int v)
{
E[++tote].u=u,E[tote].v=v;
E[tote].nex=head[u];head[u]=tote;
}
int dfn[maxn],low[maxn],cnt=0,s[maxn]/*栈*/,ins[maxn],top;
int scc[maxn],num=0; // 结点 i 所在 SCC 的编号、当前的scc个数
void tarjan(int x)
{
low[x]=dfn[x]=++cnt;
s[++top]=x;ins[x]=1;
for(int i=head[x];i;i=E[i].nex)
{
int v=E[i].v;
if(!dfn[v])
{
tarjan(v);
low[x]=min(low[x], low[v]);
}
else if(ins[v])
low[x]=min(low[x],dfn[v]);
}
if(dfn[x]==low[x])
{
++num;
while(s[top]!=x)
{
scc[s[top]]=num;
ins[s[top]]=0;
top--;
}
scc[s[top]]=num;
ins[s[top]]=0;
--top;
}
}
void reset()
{
memset(head,0,sizeof head);
memset(dfn,0,sizeof dfn);
memset(low,0,sizeof low);
memset(ins,0,sizeof ins);
memset(s,0,sizeof s);
top=tote=cnt=num=0;
}
inline int read()
{
int x=0,f=1;
char c=getchar();
while('0'>c||c>'9')
{
if(c=='-')f=-1;
c=getchar();
}
while('0'<=c&&c<='9')
{
x=(x<<1)+(x<<3)+(c^48);
c=getchar();
}
return x*f;
}
//1~n 'm' n+1~2n 'h'
char tmp1[10],tmp2[10];
void trans(char a1[],char a2[])
{
int c1=0,c2=0;
for(int i=1;i<strlen(a1);i++)
c1=c1*10+a1[i]-'0';
for(int i=1;i<strlen(a2);i++)
c2=c2*10+a2[i]-'0';
add(a1[0]=='m'?c1+n:c1,a2[0]=='m'?c2:c2+n);
}
int main()
{
int t=read();
while(t--)
{
reset();
n=read(),m=read();
for(int i=1;i<=m;i++)
{
scanf("%s %s",tmp1,tmp2);
trans(tmp1,tmp2),trans(tmp2,tmp1);
}
for(int i=1;i<=n*2;i++)if(!dfn[i])tarjan(i);
int flag=0;
for(int i=1;i<=n;i++)
{
if(scc[i]==scc[i+n])
{
flag=1;
break;
}
}
if(flag==1)puts("BAD");
else puts("GOOD");
}
return 0;
}
/*
2
3 4
m3 h1
m1 m2
h1 h3
h3 m2
2 4
h1 m2
m2 m1
h1 h2
m1 h2
*/
T2部落划分
一眼最小生成树(传统艺能了)
连一条有效边就让集合减少1个,初始n个,目标k个,所以只用连n-k个就行,连完了n-k个边后,下一个可以合并集合的边就是要求的答案了
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+100;
double calc(double x1,double y1,double x2,double y2){return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));}
int n,k;int x,y;
struct Edge{int u,v;double w;}E[maxn];int tote;
struct point{int x;int y;}p[maxn];
bool cmp(Edge a,Edge b){return a.w<b.w;}
int f[maxn];
int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-')f=-1;
c=getchar();
}
while('0'<=c&&c<='9')
{
x=(x<<1)+(x<<3)+(c^48);
c=getchar();
}
return x*f;
}
int main()
{
freopen("group.in","r",stdin);
freopen("group.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)p[i].x=read(),p[i].y=read(),f[i]=i;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
E[++tote].u=i,E[tote].v=j,E[tote].w=calc(p[i].x,p[i].y,p[j].x,p[j].y);
sort(E+1,E+tote+1,cmp);
int cnt=0;int flag=0;
for(int i=1;i<=tote;i++)
{
if(cnt==n-k)
{
flag=1;
}
int x=find(E[i].u),y=find(E[i].v);
if(x!=y)
{
cnt++;
if(flag==1)
{
printf("%.2lf",E[i].w);
break;
}
f[x]=y;
}
}
return 0;
}
/*
4 2
0 0
0 1
1 1
1 0
1.00
*/
/*
9 3
2 2
2 3
3 2
3 3
3 5
3 6
4 6
6 2
6 3
2.00
*/
T3冷冻波
题目是这样的,给出猎人、猎物、和障碍物,一个猎人在\(0\)时刻时可以击杀一个猎物(前提是猎物在猎人的射程范围之内)。猎人有坐标、攻击半径、攻击冷却;猎物只有坐标;障碍物有坐标和半径,(其实就是一个圆)。现在求如果能击杀所有的猎物,求最小的击杀时间;或者不能全部击杀,就输出\(-1\).
分析
这道题是计算几何是非常明显的,很容易想到我们可以直接预处理出每个猎人能够击杀的猎物,然后再做后续的处理,那么我们如何预处理呢?考试的时候我想的是对于任何一个猎人点和障碍物圆,他们之间至多存在两条切线,那么我们只要算出这两条切线,就可以处理出当前能够被猎人打到的猎物,可惜我算不来切线啊。实际上不用这么,麻烦,画个图就很容易看出来
我们多多少少可以去考虑半径和垂线之间的长短关系,上图这种就是显然不行的
就像这样垂足在线段上面的时候,垂线\(\ge\)半径都是可以的
那么垂足像这样不在线段上呢,\(Obviously\),我们只用看两个端点到圆心的距离和半径的大小关系就行。
然后三层循环就可以预处理出所有猎人和猎物了。
但是很少有人能想到后续处理是网络最大流,当时我都想打爆搜了。。为什么是网络流呢?因为一个猎人可以击杀多个猎物,换句话说,一个猎物可以被多个猎人击杀,相当于所有猎人向猎物建了一条流量为\(1\)的边,源点0向每个猎人建一条边,流量为当前总时间t内他们能够击杀的猎物数\(T/cd[i]+1\)(\(0\)时刻的时候也可以打一个),在从每个猎物向一个超级远点建边(只用被击杀一次)。那么现在要求的就是当前总时间下最大流是否为总猎物数,那么这个最大时间一定是具有单调性的(多了就可能会有剩余,少了就杀不完),那么对于这种单调性问题我们就可以采用二分来解决。每次只改变\(0\)向每个猎人连出的边权,然后跑最大流直到答案收敛。
\(Code\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=500000;
int n,m,k,s,t;
int dis[maxn],vis[maxn],pre[maxn];
int w[maxn],v[maxn],nex[maxn];int wx[maxn];
struct witch{int x,y,r;int t;}a[maxn];
struct Target{int x,y;}c[maxn];
struct Tree{int x,y,r;}rt[maxn];
int tote=1,head[maxn];
inline void add(int uu,int vv,int ww)
{
v[++tote]=vv;w[tote]=ww;nex[tote]=head[uu];head[uu]=tote;
v[++tote]=uu;w[tote]=0;nex[tote]=head[vv];head[vv]=tote;
}
inline int bfs()
{
memset(vis,0,sizeof vis);
queue<int> q;
q.push(s);
vis[s]=1;
dis[s]=2005120700;
while(!q.empty())
{
int x=q.front();
q.pop();
for(register int i=head[x];i;i=nex[i])
{
if(w[i]==0) continue;
int vv=v[i];
if(vis[vv]==1) continue;
dis[vv]=min(dis[x],w[i]);
pre[vv]=i;
q.push(vv);
vis[vv]=1;
if(vv==t)
return 1;
}
}
return 0;
}
int ans;
inline void update()
{
int x=t;
while(x!=s)
{
int vv=pre[x];
w[vv]-=dis[t];
w[vv^1]+=dis[t];
x=v[vv^1];
}
ans+=dis[t];
}
void max_stream()
{
memset(dis,0,sizeof dis);
memset(pre,0,sizeof pre);
ans=0;
while(bfs()!=0)
update();
}
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-')f=-1;
c=getchar();
}
while('0'<=c&&c<='9')
{
x=(x<<1)+(x<<3)+(c^48);
c=getchar();
}
return x*f;
}
void input()
{
for(int i=1;i<=n;i++)a[i].x=read(),a[i].y=read(),a[i].r=read(),a[i].t=read();
for(int i=1;i<=m;i++)c[i].x=read(),c[i].y=read();
for(int i=1;i<=k;i++)rt[i].x=read(),rt[i].y=read(),rt[i].r=read();
}
double calc(int x1,int y1,int x2,int y2){return sqrt(1.0*(x1-x2)*(x1-x2)+1.0*(y1-y2)*(y1-y2));}
bool jud(witch a,Target b,Tree c)
{
int A=b.y-a.y,B=a.x-b.x,C=b.x*a.y-a.x*b.y;double d;
double fx=(B*B*c.x-A*B*c.y)*1.0/(A*A+B*B),fy=(-A*B*c.x+A*A*c.y-B*C)*1.0/(A*A+B*B);
if(calc(a.x,a.y,fx,fy)+calc(fx,fy,b.x,b.y)==calc(a.x,a.y,b.x,b.y))d=min(calc(a.x,a.y,c.x,c.y),calc(b.x,b.y,c.x,c.y));
//这个地方的判断条件一定不能是if((fx<=a.x&&fx<=b.x)||(fy<=a.y&&fy<=b.y)||(fx>=a.x&&fx>=b.x)||(fy>=a.y&&fy>=b.y))d=min(calc(a.x,a.y,c.x,c.y),calc(b.x,b.y,c.x,c.y));
else d=abs(A*c.x+B*c.y+C)*1.0/sqrt(A*A+B*B);
return c.r<=d;
}
/*a,c,rt*/
int able[maxn];
int rec[maxn],cur;
int main()
{
n=read(),m=read(),k=read();
s=0,t=n+m+10;
input();
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(calc(a[i].x,a[i].y,c[j].x,c[j].y)>=a[i].r)continue;
int flag=1;
for(int t=1;t<=k;t++)
if(!jud(a[i],c[j],rt[t]))flag=0;
if(flag)
{
able[j]=1;
add(i,j+n,1);
}
}
}
for(int i=1;i<=n;i++)add(0,i,0),rec[++cur]=tote-1;
for(int i=1;i<=m;i++)
{
add(i+n,t,1);
if(!able[i])
{
printf("-1");
return 0;
}
}
int l=0,r=1<<25;
int fans=r;
while(l<=r)
{
int mid=(l+r)>>1;
memcpy(wx,w,sizeof wx);
for(int i=1;i<=cur;i++)
w[rec[i]]=mid/a[v[rec[i]]].t+1;
max_stream();
if(ans==m)r=mid-1,fans=mid;
else l=mid+1;
memcpy(w,wx,sizeof w);
}
printf("%d",fans);
return 0;
}
注意
最后还是一些很琐碎的东西,比如我们需要一个数组来记录跑最大流之前的边权,跑完了再重新复制给原数组。还有就是每次最大流之前要清空记录流量的\(dis[i]\)数组,还有\(pre[i]\)数组。
T4蔬菜庆典
芝士一道还没补完的题
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/16717192.html