ZJNU 2022-02-26 个人排位赛1 部分题解

2022/02/27 UPD:

  • 更正 \(C\) 题公比少了平方
  • 开头补充 INF 的数值
  • \(I\) 题补充特判

写在前面:

文中使用到的宏定义等参照此处,下文不再重复出现

#define closeSync ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define multiCase int T;cin>>T;for(int t=1;t<=T;t++)
#define rep(i,a,b) for(auto i=(a);i<=(b);i++)
#define repp(i,a,b) for(auto i=(a);i<(b);i++)
#define per(i,a,b) for(auto i=(a);i>=(b);i--)
#define perr(i,a,b) for(auto i=(a);i>(b);i--)
#define REP(i,a,b,s) for(auto i=(a);i<=(b);i+=s)
#define REPP(i,a,b,s) for(auto i=(a);i<(b);i+=s)
#define PER(i,a,b,s) for(auto i=(a);i>=(b);i-=s)
#define PERR(i,a,b,s) for(auto i=(a);i>(b);i-=s)
#define mst(a,b) memset(a,b,sizeof(a))
#define pb push_back
typedef long long ll;
typedef pair<int,int> pii;
const int INF=0x3f3f3f3f;

A - An (Almost) Perfect Match

没读,题解好像是记忆化搜索

B - Good Coin Denomination

给定已按升序排序的至多 \(10\) 个数字 \(\{a\}\),问是否满足 \(2a_i\le a_{i+1}\)

直接判断

void solve()
{
    int n;
    cin>>n;
    int a[n];
    cout<<"Denominations:";
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        cout<<' '<<a[i];
    }
    cout<<'\n';
    for(int i=1;i<n;i++)
        if(a[i]<a[i-1]*2)
        {
            cout<<"Bad coin denominations!\n\n";
            return;
        }
    cout<<"Good coin denominations!\n\n";
}
signed main()
{
    closeSync;
    multiCase
    {
        solve();
    }
    return 0;
}

C - Cameron’s Crazy Circles

给定直角三角形的两直角边 \(a,b\ (a\lt b)\)

定义其第三条边为 \(c\),并绘制出其内接三角形 \(C_1\)

再沿着较长的直角边 \(b\) 继续绘制与 \(b,c\)\(C_1\) 相切的内圆 \(C_2\)

pic_c1

如此循环下去,绘制出无限个圆 \(C_i\)

问所有绘制出的圆的面积与该三角形的面积之比,即 \(\frac{\sum C_i}{S_{\triangle}}\)

首先求出最大的内接圆的半径 \(R\),由于是直角三角形,所以公式直接得出 \(R=\frac{a+b-c}2\)

(不知道这条公式的就先求两条角平分线,得出交点后来算 \(R\) 吧,见代码)

根据勾股定理,算得所有可以绘制出的圆的直径和 \(totLen=\sqrt{R^2+(b-R)^2}+R\)

pic_c2

转为另一种方式看 \(C_1,C_2\) 两个圆,绘制出两相似等腰三角形,见下图蓝线

pic_c3

可得小三角形与大三角形的边长比即 \(ratio = \frac{totLen-2R}{totLen} = 1-\frac{2R}{totLen}\)

由圆形面积公式 \(S_1=\pi R^2\) ,可知面积与边长呈平方关系

\(C_2\) 的半径 \(R_2=R\times ratio\),圆 \(C_2\) 的面积即 \(S_2=\pi R_2^2=\pi R^2ratio^2=ratio^2S_1\)

无限绘制圆形,比例不变,得 \(S_{i+1}=ratio^2S_i\)

这是一个首项为 \(S_1=\pi R^2\),公比为 \(ratio^2\) 的等比数列

\(\sum S_i = \frac {S_1(1-ratio^{\infin})}{1-ratio^2} = \frac{S_1}{1-ratio^2}\)

答案即 \(\frac{2\sum S}{a\times b}\)

void solve()
{
    double a,b;
    cin>>a>>b;
    
    Point p1=Point(0,0);
    Point p2=Point(0,a);
    Point p3=Point(b,0);
    
    Vector v21=p1-p2;
    Vector v23=p3-p2;
    Vector v2f=Rotate(v21,Angle(v21,v23)/2); // 旋转一半求角平分向量,板子太长不放了
    
    Line la=Line(p1,Point(1,1));
    Line lb=Line(p2,p2+v2f);
    Point cross=la.intersectPoint(lb); // 求出内接圆圆心
    
    double R=cross.x; // R=(a+b-c)/2 ,上面可以都不用写
    double S=PI*R*R;
    
    double totLen=sqrt(R*R+(b-R)*(b-R))+R;
    double rt=1-R*2/totLen;
    
    double totS=S/(1-rt*rt);
    cout<<fixed<<setprecision(4)<<totS/(a*b/2)<<'\n';
}
signed main()
{
    closeSync;
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
        cout<<'\n';
    }
    return 0;
}

D - Matrix Transformation

给定一个 \(n\times m\) 的矩形,每次可以选择任意两个相邻位置,令该两位置值同时 \(+1\)\(-1\)

问是否存在一种方式能够使得矩形内每个位置的值均为 \(0\)

按照每个位置的 \(x+y\) 的奇偶性进行分组

由于每次选择的是两个相邻的位置,因此 \(x+y\) 的奇偶性一定不同

又因为两个位置的值同时操作,且改变量相同,因此本题就变成了判断两组数值之和是否相同

int a[111][111];

void solve()
{
    int n,m;
    cin>>n>>m;
    int s1=0,s2=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
        for(int j=i%2+1;j<=m;j+=2)
            s1+=a[i][j];
        for(int j=2-i%2;j<=m;j+=2)
            s2+=a[i][j];
    }
    if(s1==s2)cout<<"YES\n";
    else cout<<"NO\n";
}
signed main()
{
    closeSync;
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
        cout<<'\n';
    }
    return 0;
}

E - Cut the Cake!

按相邻边的顺序给定一个多边形蛋糕,被 \(y=Y\) 的一条平行于 \(x\) 轴的直线切成上下两部分

保证蛋糕一定会被切割到,且不会切割到蛋糕的任意顶点上

问两部分蛋糕的周长分别是多长

定义两个多边形容器 \(poly1/poly2\) 存两部分蛋糕的每个点

从任意某条边开始,枚举所有边,并判断每条边是否会被 \(y=Y\) 切割

如果不会被切割,则将上一个点归为当前的多边形容器内

如果边 \(p_ip_{i+1}\) 被切割,交点为 \(pc\),则将 \(pc\) 放在当前多边形容器内,接下来切换为另一个容器来收集点(一旦产生交点即表示跨越了 \(y=Y\) 这条线,前一个点与后一个点应当归属于不同的多边形容器)

pic_e

如上图,按照顺序加入多边形的点如右侧所示(交点 \(pc_i\) 应当同时存在两个多边形中)

得到的两个新多边形,可以直接通过两两相邻点距离之和求出周长

Point p[10];
vector<Point> poly[2];

void solve()
{
    for(int t=0;t<2;t++)
        poly[t].clear();
    
    int n,y;
    cin>>n>>y;
    Line ly=Line(Point(0,y),Point(1,y));
    for(int i=0;i<n;i++)
        cin>>p[i].x>>p[i].y;
    
    int cur=0;
    for(int i=0;i<n;i++) // 按顺序枚举每条边 p[i]p[(i+1)%n]
    {
        poly[cur].pb(p[i]);
        if((p[i].y>y)!=(p[(i+1)%n].y>y)) // 判断i与(i+1)%n两点是否位于y=Y这条线两侧
        {
            Point pp=Line(p[i],p[(i+1)%n]).intersectPoint(ly); // 获得交点
            poly[cur].pb(pp);
            cur^=1; // 切换另一个容器继续收集点
            poly[cur].pb(pp);
        }
    }
    
    double S[2]={0,0};
    rep(t,0,1)
        repp(i,0,poly[t].size()) // 遍历两个多边形容器内每条边,直接求距离和即可
            S[t]+=Distance(poly[t][i],poly[t][(i+1)%poly[t].size()]);
    
    if(S[0]>S[1])
        swap(S[0],S[1]);
    cout<<S[0]<<' '<<S[1]<<'\n';
}
signed main()
{
    cout<<fixed<<setprecision(3);
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
        cout<<'\n';
    }
    return 0;
}

F - Camp Out

每天 \(24\) 小时被分为 \(6\) 个时间段 \(0\sim 4,4\sim 8,\cdots,20\sim 24\) (左闭右开)

你需要安排 \(10\) 个人,在一个星期 \(7\)\(168\) 个小时内搭帐篷,且需满足以下限制:

  • 每个时段至少有 \(3\) 个人搭帐篷
  • 只有整个时间段内 \(4\) 小时都有空的人才可以安排该时间段给他
  • 每个人至多被安排 \(80\) 个小时

现在给定每个人没有空的时间段,问是否存在一种合法安排方案?

标准二分图权值匹配题,下面考虑用最大流解(KM也可以)

首先找出二分图两侧点的代表,明显一侧指人,一侧指时间段

源点向每个人连边,流量为 \(20\),表示每个人至多被安排 \(20\) 个时间段

每个时间段向汇点连边,流量为 \(3\),这里贪心让每个时间段只安排 \(3\) 人就行,最后判断网络是否能够流满即可

对于每个人 \(i\),只要时间段 \(j\) 有空,就从 \(i\)\(j\) 连一条流量为 \(1\) 的边,表示该人可以安排时间段 \(j\)

最后判断最大流是否等于 \(168/4*3=126\) 即可

int bz[55];

void solve()
{
    init(54);
    rep(i,1,10)
        addedge(53,i,20);
    rep(i,11,52)
        addedge(i,54,3);
    rep(i,1,10)
    {
        mst(bz,0);
        int n;
        cin>>n;
        rep(j,1,n)
        {
            int d,s,e;
            cin>>d>>s>>e;
            s=s/4+(d-1)*6+11;
            e=(e-1)/4+(d-1)*6+11;
            rep(k,s,e)
                bz[k]=1; //表示该时间段没空
        }
        rep(k,11,52)
            if(!bz[k])
                addedge(i,k,1); //有空的连边
    }
    int f=dinic(53,54);
    //cout<<f<<'\n';
    cout<<(f==126?"YES\n":"NO\n");
}
signed main()
{
    closeSync;
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
        cout<<'\n';
    }
    return 0;
}

G - Sorry About That, Chief!

判断给定数是否是素数,如果不是,问与最近的素数差值是多少

素数筛,每个询问暴力即可

bool vis[22222];
void init()
{
    rep(i,2,20000)
        if(!vis[i])
            REP(j,i*i,20000,i)
                vis[j]=true;
}

void solve()
{
    int n;
    cin>>n;
    cout<<"Input value: "<<n<<'\n';
    if(!vis[n])
        cout<<"Would you believe it; it is a prime!\n\n";
    else
    {
        rep(i,1,1000)
        {
            if(!vis[n-i]||!vis[n+i])
            {
                cout<<"Missed it by that much ("<<i<<")!\n\n";
                return;
            }
        }
    }
}
signed main()
{
    closeSync;
    init();
    multiCase
    {
        solve();
    }
    return 0;
}

H - Knight Moves – Gold Edition

I - Knight Moves – Black Edition

\(n\times n\) 的国际象棋棋盘上,骑士从 \((r_1,c_1)\) 走到 \((r_2,c_2)\) 的最少步数

EZ解

bfs

int d[25][25];
const int ddx[8]={-2,-2,-1,-1,1,1,2,2};
const int ddy[8]={-1,1,-2,2,-2,2,-1,1};

void solve()
{
    int n,r1,c1,r2,c2;
    cin>>n>>r1>>c1>>r2>>c2;
    mst(d,INF);
    d[r1][c1]=0;
    queue<pii> q;
    q.push(pii(r1,c1));
    while(!q.empty())
    {
        pii p=q.front();
        q.pop();
        repp(i,0,8)
        {
            int px=ddx[i]+p.first;
            int py=ddy[i]+p.second;
            if(px>0&&py>0&&px<=n&&py<=n&&d[px][py]>d[p.first][p.second]+1)
            {
                d[px][py]=d[p.first][p.second]+1;
                q.push(pii(px,py));
            }
        }
    }
    cout<<d[r2][c2]<<'\n'<<'\n';
}
signed main()
{
    closeSync;
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
    }
    return 0;
}

HD解

大范围贪心 + 小范围暴力bfs

为了方便,根据对称轴翻转,使得 \((r_1,c_1)\) 出现在 \((r_2,c_2)\) 的左下角

pic_i1

其后再根据点 \((r_1,c_1)\) 位于那一块区域内进行分类讨论

如果点位于上图区域 \(1\) 内,贪心可知刚开始每次都走 \((+2,+1)\) 最优,直到 \(c1=c2\) 时,再反复 \((+2,-1),(+2,+1)\) 向右走,如下图所示:

pic_i2

如果点位于上图区域 \(4\) 内,贪心可知刚开始每次都走 \((+1,+2)\) 最优,直到 \(r1=r2\) 时,再反复 \((-1,+2),(+1,+2)\) 向上走,如下图所示:

pic_i3

如果位于上图区域 \(2\) 内,可以发现先走 \((+1,+2)\),直到与 \(y=\frac12x\) 这条直线接触时再一直走 \((+2,+1)\) 最优,但这样的走法可能比较难处理?

这种走法实际上与先接触 \(y=x\) 这条直线,再反复 \((+1,+2),(+2,+1)\) 所需要的步数是相同的,也好写OvO

区域 \(3\) 内也是相同,先靠近 \(y=x\) 再向右上走,如下图所示:

pic_i4

当然,以上也只是大范围贪心,小范围内这种走法可能会出问题

因此,最终快要接近终点的时候,限制一下不要直接取到最近的位置,稍微留出个 \(20\times 20\) 大小的位置来跑 EZ 版本的代码即可

int d[35][35];
const int ddx[8]={-2,-2,-1,-1,1,1,2,2};
const int ddy[8]={-1,1,-2,2,-2,2,-1,1};

int solve2(int n,int r1,int c1,int r2,int c2)
{
    mst(d,INF);
    d[r1][c1]=0;
    queue<pii> q;
    q.push(pii(r1,c1));
    while(!q.empty())
    {
        pii p=q.front();
        q.pop();
        repp(i,0,8)
        {
            int px=ddx[i]+p.first;
            int py=ddy[i]+p.second;
            if(px>0&&py>0&&px<=n&&py<=n&&d[px][py]>d[p.first][p.second]+1)
            {
                d[px][py]=d[p.first][p.second]+1;
                q.push(pii(px,py));
            }
        }
    }
    return d[r2][c2];
}

void solve()
{
    ll n,r1,c1,r2,c2;
    cin>>n>>r1>>c1>>r2>>c2;
    
    // 如果范围较小,直接调用EZ Ver.的解法
    if(n<=20)
    {
        cout<<solve2(n,r1,c1,r2,c2)<<'\n';
        return;
    }
    
    //翻转点,使得点1位于点2左下,此后只需要考虑x++和y++
    if(r1>r2)
    {
        r1=n-r1+1;
        r2=n-r2+1;
    }
    if(c1>c2)
    {
        c1=n-c1+1;
        c2=n-c2+1;
    }
    
    ll ans=0;
    if(r2-r1>c2-c1) //如果x方向的增量比y方向的增量多,位于上图区域3,4
    {
        if(r1+(c2-c1)*2<=r2) //上图区域4
        {
            // 先 (+1,+2) 走到 c1=c2 的位置
            ll d=c2-c1;
            ans+=d;
            r1+=d*2;
            c1=c2;
            // 再反复向上,每两步可以使 y+4
            d=(r2-r1)/4-1; // 注意这里的 -1,为后面的暴力留出空间,下同
            if(d>0)
            {
                ans+=d*2;
                r1+=d*4;
            }
        }
        else //上图区域3
        {
            ll d=(r2-r1)-(c2-c1);
            ans+=d;
            r1+=d*2;
            c1+=d;
            
            d=min(r2-r1,c2-c1)/3-1;
            if(d>0)
            {
                ans+=d*2;
                r1+=d*3;
                c1+=d*3;
            }
        }
    }
    else
    {
        if(c1+(r2-r1)*2<=c2) //上图区域1
        {
            ll d=r2-r1;
            ans+=d;
            c1+=d*2;
            r1=r2;
            
            d=(c2-c1)/4-1;
            if(d>0)
            {
                ans+=d*2;
                c1+=d*4;
            }
        }
        else //上图区域2
        {
            ll d=(c2-c1)-(r2-r1);
            ans+=d;
            c1+=d*2;
            r1+=d;
            
            d=min(r2-r1,c2-c1)/3-1;
            if(d>0)
            {
                ans+=d*2;
                r1+=d*3;
                c1+=d*3;
            }
        }
    }
    
    // 最后差的一点点就暴力跑,特判边界
    if(r1<=4&&c1<=4) // 左下边界
        ans+=solve2(20,r1,c1,r2,c2);
    else if(r2>=n-3&&c2>=n-3) // 右上边界
        ans+=solve2(20,20-(n-r1),20-(n-c1),20-(n-r2),20-(n-c2));
    else
        ans+=solve2(20,10,10,10+(r2-r1),10+(c2-c1));
    cout<<ans<<'\n';
}
signed main()
{
    closeSync;
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
        cout<<'\n';
    }
    return 0;
}

J - A Prickly Problem – Gold Edition

K - A Prickly Problem – Black Edition

给定一张仙人掌图(两两简单环最多只有一个公共点)

可以去除一些边使其变成一棵树,问生成树数量

解1

求生成树的数量,裸题

矩阵树定理,求无向连通图生成树计数,套高斯消元板子即可

没代码QAQ

解2

由于给定的图是仙人掌,明显答案就是每个简单环内的边数之乘积(由于生成树不存在环,因此每个简单环都可以任意选择一条边去掉)

考虑点双连通分量,求出每条边所在的 bcc 编号,再统计出每个 bcc 包含的边数,求乘积即可

没代码\(^2\)QAQ

解3

原理同解2,但可以直接根据 dfs 搜索出每个环内的边数

从任意一个点开始搜索,为每个搜到的点分配深度 \(depth\)

如果某个点此前没有被搜到,则朝下继续搜索

如果已经被搜索到,则此时的搜索深度 \(d\) 与原先为该点分配的深度 \(depth\) 的差值即环内边数

最后向上传递解即可

int n,m;
vector<int> G[50050];
int dep[50050];
bool vis[50050];

int dfs(int u,int fa,int d)
{
    if(vis[u])
        return max(d-dep[u],1);
    dep[u]=d;
    vis[u]=true;
    int r=1;
    for(int &v:G[u])
    {
        if(v==fa)
            continue;
        r=r*dfs(v,u,d+1)%mod;
    }
    return r;
}

void solve()
{
    cin>>n>>m;
    rep(i,1,n)
    {
        G[i].clear();
        dep[i]=0;
        vis[i]=false;
    }
    rep(i,1,m)
    {
        int u,v;
        cin>>u>>v;
        G[u].pb(v);
        G[v].pb(u);
    }
    cout<<dfs(1,-1,1)<<'\n';
}
signed main()
{
    closeSync;
    multiCase
    {
        cout<<"Case #"<<t<<": ";
        solve();
        cout<<'\n';
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值