2020/11/05 CSP模拟赛
Preface
临走前的最后一场模拟赛竟然不是信心赛,我当场裂开
还好后来稳住心态写了T1,T3,T4推了下写个个比较麻烦的\(60\),加上陈指导给教我的T2\(50\)苟了点颜面
射线
Problem
- 有两条以\((0,0)\)为端点, 分别经过\((a,b),(c,d)\)的射线
- 求出夹在两条射线中间, 且距离\((0,0)\)最近的点\((x,y)\)
Solution
陈指导看出了这其实就是BZOJ #2187. fraction的经典题,直接上类欧几里得即可
首先直接大力分类讨论:
- \(\frac{a}{b}\)与\(\frac{c}{d}\)之间存在整数时,必然选择其中最小的整数
- 当\(a=0\)时,此时问题转化为\(\frac{p}{q}<\frac{c}{d}\),移下项变成\(q>\frac{dp}{c}\),令\(p=1\)即可解出\(q=\lfloor \frac{d}{c}\rfloor+1\)
- 当\(a<b\)时,我们可以把所有数都取倒数,此时\(\frac{d}{c}<\frac{q}{p}<\frac{b}{a}\),递归处理即可
- 当\(a\ge b\)时,我们只保留\(\{\frac{a}{b}\}\),令\(t=\lfloor \frac{a}{b}\rfloor\),此时有\(\frac{a\%b}{b}<\frac{p-qt}{q}<\frac{c-dt}{d}\),递归处理即可
总复杂度\(O(T\log n)\)
#include<cstdio>
#include<cctype>
#include<iostream>
#define RI register int
#define CI const int&
#define Tp template <typename T>
#define int long long
using namespace std;
int t,a,b,c,d,x,y;
class FileInputOutput
{
private:
static const int S=1<<21;
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[25];
public:
FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
Tp inline void read(T& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
Tp inline void write(T x,const char& ch)
{
if (x<0) pc('-'),x=-x; RI ptop=0; while (pt[++ptop]=x%10,x/=10);
while (ptop) pc(pt[ptop--]+48); pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
#undef tc
#undef pc
}F;
inline void solve(CI a,CI b,CI c,CI d,int& x,int& y)
{
if (a/b+1<=(c-1)/d) return (void)(x=a/b+1,y=1); if (!a) return (void)(x=1,y=d/c+1);
if (a<b) solve(d,c,b,a,x,y),swap(x,y); else solve(a%b,b,c-(a/b)*d,d,x,y),x+=(a/b)*y;
}
signed main()
{
freopen("ray.in","r",stdin); freopen("ray.out","w",stdout);
for (F.read(t);t;--t)
{
F.read(a); F.read(b); F.read(c); F.read(d);
if (a*d>c*b) swap(a,c),swap(b,d);
if (!a) { F.write(1,' '); F.write(d/c+1,'\n'); continue; }
if (!d) { F.write(a/b+1,' '); F.write(1,'\n'); continue; }
solve(a,b,c,d,x,y); F.write(x,' '); F.write(y,'\n');
}
return F.flush(),0;
}
图
Problem
- 给一张带点权的联通二分图
- 多组询问两点\(x,y\)间路径的权值最大值
- 一条路径的权值定义为这条路径上所有点的异或和
- 一个点可以被经过多次,并计算多次答案
Solution
STO CXR ORZ
考虑当站在一个点\(x\)上不动时,我们从\(x\)走到\(y\)再走回\(x\),相当于同时改变了\(x\)和\(y\)的选择状态,且对其它点都无影响
那么显然我们只需要改变\(x,y\)的选取状态,再改变\(x,z\)的选取状态,就可以改变任意两点的选择状态
也就是说,在不动的时候,我们可以在不改变选择点数奇偶性的前提下任选,一旦走了一步,相当于选择点数奇偶性就改变了
由于这道题有一个很优秀的性质——联通二分图,因此我们只需要给所有点黑白染色之后就可以快速判断答案(要么是选奇数个数,要么是选偶数个数)
考虑用线性基维护,偶数个数的异或最大值可以考虑把所有相邻位置的异或值都扔进线性基里,因为此时若它们在原序列中有重复那么异或之后就变成选了\(3-1=2\)个数,若不重复就变成选了\(2+2=4\)个数
考虑奇数的情况,我们枚举每一个位置,强制它一定选,在之前的线性基里查询即可
预处理\(O(n\log a_i)\),询问\(O(1)\)(就两个值,怪不得不发大样例)
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cctype>
#define RI register int
#define CI const int&
#define Tp template <typename T>
using namespace std;
const int N=500005;
class FileInputOutput
{
private:
static const int S=1<<21;
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[25];
public:
FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
Tp inline void read(T& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
Tp inline void write(T x,const char& ch='\n')
{
if (x<0) pc('-'),x=-x; RI ptop=0; while (pt[++ptop]=x%10,x/=10);
while (ptop) pc(pt[ptop--]+48); pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
#undef tc
#undef pc
}F;
struct edge
{
int to,nxt;
}e[N<<1]; int n,m,q,x,y,head[N],cnt,col[N],a[N],ans[2];
struct Linear_Basis
{
static const int R=31; int r[R];
inline void insert(int x)
{
for (RI i=R-1;~i;--i) if ((x>>i)&1)
{
if (!r[i]) { r[i]=x; break; } x^=r[i];
}
}
inline int query(int ret=0)
{
for (RI i=R-1;~i;--i) if ((ret^r[i])>ret) ret^=r[i]; return ret;
}
}L;
inline void addedge(CI x,CI y)
{
e[++cnt]=(edge){y,head[x]}; head[x]=cnt;
e[++cnt]=(edge){x,head[y]}; head[y]=cnt;
}
#define to e[i].to
inline void paint(CI now)
{
for (RI i=head[now];i;i=e[i].nxt) if (!~col[to]) col[to]=col[now]^1,paint(to);
}
#undef to
int main()
{
freopen("graph.in","r",stdin); freopen("graph.out","w",stdout);
RI i; for (F.read(n),F.read(m),F.read(q),i=1;i<=n;++i) F.read(a[i]);
for (i=1;i<n;++i) L.insert(a[i]^a[i+1]); ans[0]=L.query(0);
for (i=1;i<=n;++i) ans[1]=max(ans[1],L.query(a[i]));
for (i=1;i<=m;++i) F.read(x),F.read(y),addedge(x,y);
for (memset(col,-1,sizeof(col)),col[1]=1,paint(1),i=1;i<=q;++i)
F.read(x),F.read(y),F.write(ans[col[x]!=col[y]]);
return F.flush(),0;
}
找钱
Problem
- 有\(n\)种面值\(a_i\)的货币,每种货币你有一定数量\(b_i\),同时店员手中也有一定数量\(c_i\)
- 你要购买一个价格为\(X\)的物品,假设你支付的钱为\(Y(Y\ge X)\),那么店员找回的\(Y-X\)的纸币中不能与你支付的纸币有交集
- 求所有合法的付钱—找钱方案数
Solution
比较简单的DP题,现在感觉这些DP确实都比较一眼
显然我们可以发现无论何时你支付的使用的货币在对\(a_i\)上升排序后一定在店员使用的后面
那么我们容易设计出状态\(f_{i,j}\)表示后\(i\)种货币组成的总价格为\(j\)的方案数,\(g_{i,j}\)表示前\(i\)种货币组成的总价格为\(j\)的方案数
但是我们发现要不重不漏地计算答案还要强制第\(i\)种货币使用,再开一个状态表示一下即可
考虑转移的时候如果暴力枚举拿了几个是\(O(n\times b_i \times X)\)的,题解使用了二进制分组优化,这样会带一个\(\log\)
但是陈指导教我了一个更神奇的做法,考虑我们每次转移从\(j-k\times a_i\)转移到\(j\),我们会发现我们要转移的状态都是关于\(a_i\)同余的
或者更具体地,我们发现我们记下第二维模\(a_i\)同余的位置的最后\(b_i\)个的和,每次转移时维护一下即可
然后今天才理解了多重背包的\(O(nm)\)写法其实也就是基于这个思路,这道题是求和,如果是求最值就开个单调队列即可
总复杂度\(O(nX)\)
#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005,M=10005,mod=1e9+7;
int n,m,a[N],b[N],c[N],f[N][M<<1],ff[N][M<<1],g[N][M<<1],s[M],ct[M],ans;
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
if ((x-=y)<0) x+=mod;
}
int main()
{
freopen("deal.in","r",stdin); freopen("deal.out","w",stdout);
RI i,j; for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
scanf("%d%d%d",&a[i],&b[i],&c[i]);
for (f[n+1][0]=1,i=n;i;--i)
{
for (j=0;j<a[i];++j) f[i][j]=s[j]=f[i+1][j],ct[j]=0;
for (j=a[i];j<m+a[n];++j)
{
int p=j%a[i]; inc(s[p],f[i+1][j]); if (++ct[p]>b[i])
--ct[p],dec(s[p],f[i+1][j-a[i]*(b[i]+1)]); inc(f[i][j],s[p]);
}
for (j=0;j<a[i];++j) s[j]=f[i+1][j],ct[j]=0;
for (j=a[i];j<m+a[n];++j)
{
int p=j%a[i]; if (++ct[p]>b[i]) --ct[p],dec(s[p],f[i+1][j-a[i]*(b[i]+1)]);
inc(ff[i][j],s[p]); inc(s[p],f[i+1][j]);
}
}
for (g[0][0]=i=1;i<n;++i)
{
for (j=0;j<a[i];++j) g[i][j]=s[j]=g[i-1][j],ct[j]=0;
for (j=a[i];j<m+a[n];++j)
{
int p=j%a[i]; inc(s[p],g[i-1][j]); if (++ct[p]>c[i])
--ct[p],dec(s[p],g[i-1][j-a[i]*(c[i]+1)]); inc(g[i][j],s[p]);
}
}
for (i=1;i<=n;++i) for (j=m;j<m+a[i];++j)
inc(ans,1LL*g[i-1][j-m]*ff[i][j]%mod); return printf("%d",ans),0;
}
几何
Problem
- 平面直角坐标系内有 \(n\)个点, 你想知道有多少曲线\(y=x^2+bx+c\)经过其中的至
少两个点, 而且没有任何点在它的上方
Solution1
先讲一个我考试的时候写\(O(n^2\log n)\)时想出的辣鸡做法,应该没什么问题的说
考虑原来的限制是对\(\forall i\in [1,n],x_i^2+bx_i+c-y_i\ge 0\),我们考虑求反面\(\exist i\in [1,n],x_i^2+bx_i+c-y_i< 0\)
考虑如果我们\(O(n^2)\)求出枚举点对算出\(b,c\),此时相当于要最小化\(x_i^2+ax_i+c-y_i\),由于对于观察上面的式子,如果我们把\(a\)看成自变量那么一个点可以看作一条直线斜率\(x_i\),纵截距为\(x_i^2+c-y_i\)的直线
考虑此时我们相当于是找到当\(a\)为某个值时所有直线在该点值时的最小值
求出所有直线的下半平面的半平面交然后询问在交点数组上二分即可
再仔细思考一下就会发现答案显然只可能在上凸壳的相邻点之间,因此直接求个上凸壳即可
代码咕了,给个\(O(n^2\log n)\)的暴力意思一下
#include<cstdio>
#include<set>
#include<algorithm>
#include<assert.h>
#include<cmath>
#define RI register int
#define CI const int&
#define mp make_pair
#define int long long
#define fi first
#define se second
using namespace std;
typedef pair <double,double> pd;
const int N=1005;
const double EPS=1e-6;
struct line
{
int k,b,id;
inline line(CI K=0,CI B=0,CI Id=0) { k=K; b=B; id=Id; }
friend inline bool operator < (const line& A,const line& B)
{
return A.k!=B.k?A.k>B.k:A.b<B.b;
}
}l[N]; int n,x[N],y[N],stk[N],top,ans,cnt; double cp[N]; pd p[N*N];
inline int dcmp(const double& x)
{
if (fabs(x)<EPS) return 0; return x<0?-1:1;
}
inline double getx(const line& A,const line& B)
{
if (A.k==B.k) return -1e18; return 1.0*(B.b-A.b)/(A.k-B.k);
}
inline bool operator < (const pd& A,const pd& B)
{
if (dcmp(A.fi-B.fi)<0) return 1; if (dcmp(A.fi-B.fi)>0) return 0;
if (dcmp(A.se-B.se)<0) return 1; if (dcmp(A.se-B.se)>0) return 0; return 1;
}
inline bool operator == (const pd& A,const pd& B)
{
return !dcmp(A.fi-B.fi)&&!dcmp(A.se-B.se);
}
signed main()
{
freopen("geo.in","r",stdin); freopen("geo.out","w",stdout);
RI i,j; for (scanf("%lld",&n),i=1;i<=n;++i)
scanf("%lld%lld",&x[i],&y[i]),l[i]=line(x[i],x[i]*x[i]-y[i],i);
for (sort(l+1,l+n+1),i=1;i<=n;++i)
{
if (top&&l[stk[top]].k==l[i].k) continue;
while (top>1&&dcmp(getx(l[stk[top]],l[i])-getx(l[stk[top]],l[stk[top-1]]))<=0) --top;
stk[++top]=i;
}
for (i=1;i<top;++i) cp[i]=getx(l[stk[i]],l[stk[i+1]]); cp[top]=1e18;
for (i=1;i<n;++i) for (j=i+1;j<=n;++j) if (x[i]!=x[j])
{
double b=1.0*(y[i]-y[j]-(x[i]*x[i]-x[j]*x[j]))/(x[i]-x[j]),c=y[i]-x[i]*x[i]-b*x[i];
//assert(x[i]*x[i]+b*x[i]+c==y[i]&&x[j]*x[j]+b*x[j]+c==y[j]);
int pos=l[stk[lower_bound(cp+1,cp+top+1,b)-cp]].id;
if (dcmp(1.0*x[pos]*x[pos]+b*x[pos]+c-y[pos])>=0) p[++cnt]=mp(b,c),++ans;
}
for (sort(p+1,p+cnt+1),i=1;i<=cnt;i=j+1)
{
for (j=i;j+1<=cnt&&p[j+1]==p[i];++j); ans-=j-i;
}
return printf("%lld",ans),0;
}
Solution2
后来看了题解发现我就是个nt,我们直接把条件转化为\(\forall i\in [1,n],bx_i+c\ge y_i-x_i^2\)
右边显然是个定值,因此我们把一个点\((x_i,y_i)\)看作\((x_i,y_i-x_i^2)\),那么题目就变成了有多少条直线,经过至少两个点,且它的上方没有点
此时直接求一个上凸壳然后只有相邻两点能产生贡献,答案就是上凸壳的点数减\(1\)
#include<cstdio>
#include<algorithm>
#include<cmath>
#define RI register int
#define CI const int&
#define int long long
using namespace std;
const int N=200005;
const double EPS=1e-6;
struct Point
{
int x,y;
inline Point(CI X=0,CI Y=0) { x=X; y=Y; }
friend inline bool operator < (const Point& A,const Point& B)
{
return A.x!=B.x?A.x<B.x:A.y<B.y;
}
friend inline Point operator - (const Point& A,const Point& B)
{
return Point(A.x-B.x,A.y-B.y);
}
}a[N]; int n,x[N],y[N],top,stk[N];
typedef Point Vector;
inline int Cross(const Vector& A,const Vector& B)
{
return A.x*B.y-A.y*B.x;
}
inline int dcmp(const double& x)
{
if (fabs(x)<EPS) return 0; return x<0?-1:1;
}
signed main()
{
freopen("geo.in","r",stdin); freopen("geo.out","w",stdout);
RI i,j; for (scanf("%lld",&n),i=1;i<=n;++i)
scanf("%lld%lld",&x[i],&y[i]),a[i]=Point(x[i],y[i]-x[i]*x[i]);
for (sort(a+1,a+n+1),i=1;i<=n;++i)
{
while (top>1&&Cross(a[stk[top]]-a[stk[top-1]],a[i]-a[stk[top-1]])>=0) --top;
stk[++top]=i;
}
return printf("%d",top-1),0;
}
Postscript
STO CXR ORZ
CSP2020 RP ++