2021.icpc网络赛第二场

本文探讨了二维网格中水流模拟的算法,通过按高度排序并逐个处理节点来解决水流问题。此外,还介绍了如何处理二进制序列的加法操作,通过贪心策略和动态规划解决特定的数学问题,以及使用势能线段树优化区间欧拉函数查询。文章展示了如何将复杂问题分解并应用不同策略进行求解。
摘要由CSDN通过智能技术生成
  1. Leaking Roof 签到

大意:给定 n*n 格子,每个格子的水量为m, 当前格子的水会向周围有公共边的,且高度严格大于的格子等量流动,当且仅当高度为0时水会流出,求最终每个格子流出的水量。n<=500

思路:高度最高的格子只会往别的格子流水,直接按高度从小到大排序,模拟就好了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=510;
typedef pair<int,int> PII;
int n,m,k;
struct node{
    int h,x,y;
    bool operator <(const node &W)const
    {
        return h>W.h;
    }
}q[N*N];
int H[N][N];
double ans[N][N];
int dx[]={-1,0,1,0},dy[]={0,1,0,-1};
int main()
{ 
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            scanf("%d",&H[i][j]);
            q[k++]={H[i][j],i,j};
            ans[i][j]=m;
        }
    sort(q,q+k);
    for(int i=0;i<k;i++)
    {
        int h=q[i].h,x=q[i].x,y=q[i].y;
        int cnt=0;
        vector<PII> tmp;
        for(int j=0;j<4;j++)
        {
            int a=x+dx[j],b=y+dy[j];
            if(a<1||a>n||b<1||b>n)continue;
            if(h>H[a][b])cnt++,tmp.push_back({a,b});
        }
        if(cnt)
        {
            double fl=ans[x][y]/cnt;
            for(auto t:tmp)
            {
                int a=t.first,b=t.second;
                ans[x][y]-=fl;
                ans[a][b]+=fl;
            }  
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(H[i][j])printf("0 ");
            else printf("%.6f ",ans[i][j]);
        }
        puts("");
    }
    return 0;
}
  1. Addition 二进制、签到

大意:给定一个数 n,代表二进制的位数,sgn 代表符号 a[],b[]代表对应二进制位的数字, ∑ i n a [ i ] s g n [ i ] ∗ 2 i \sum_i^na[i]sgn[i]*2^i ina[i]sgn[i]2i

求出 相加后的结果,按上面的形式输出。30<=n<=60

思路:题目保证了一定有解,所以直接模拟就行,不用担心题目会无解。比较迷惑性的就是 sgn 符号位,如果不考虑符号位,直接模拟竖式运算求结果就行了。考虑符号其实还是竖式运算,只不过原先只有进位,这里有了符号相当于可能出现借位,模拟就好了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=65;
typedef long long LL;
int sgn[N],a[N],b[N],c[N];
int n;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=0;i<n;i++)cin>>sgn[i];
    for(int i=0;i<n;i++)cin>>a[i];
    for(int i=0;i<n;i++)cin>>b[i];
    int f=0;//纪录进位
    for(int i=0;i<n;i++)
    {
        int t=a[i]+b[i]+f*sgn[i];//若当前位是负号,从后面来的进1,相当于在这里是要减去的
        if(t==-1)c[i]=1,f=-1;
        else c[i]=t%2,f=t/2;
        f*=sgn[i];//当前位是负号的话,进1,相当于多减1,也就是对前面的借位,
    }
    for(int i=0;i<n;i++)
    {  
        cout<<c[i];
        if(i!=n-1)cout<<" ";
    }
    return 0;
}

总结:很多东西是具有相同的本质的,处理不熟悉的我们可以类比成熟悉的去处理

  1. Sort 思维、技巧

大意:给定一个长度为 n 的序列,以及 k,问每次将序列 a 按顺序分成不超多 k 段,是否能经过若干次操作使得序列 a 单调不降。若不能输出 -2,若可以但所需的最少操作次数超过 3n 次,输出 -1。否则输出操作次数,每次操作的分段方式以及每段的组合方式(n<=30000)

思路: 因为每次操作分成的段数受 k 的限制,我们从 k 的角度入手,

显然当 k 为 1时,如果序列 a 不满足条件的话就是不行的。

当 k=2时,每次分成两段,如果不交换两段的顺序相当于没变,每轮交换两段的位置实际上相当于对序列进行右移操作,并且只有一种情况可以通过平移变成单调不降的序列即类似于“567123”这样的。

当k>=3时,我们可以证明一定可以通过不超过3n次操作将序列变成单调不降,很容易证:我们每次从未排序的序列中找到最小的作为一段的开头,已经排好序的作为一段,剩下的作为另一段,即我们每次可以通过分成不超过三段最多进行 n 次使得a序列变成单调不降。对于k=1和k=2的时候我们很容易实现,主要就是k>=3时的情况。暴力查找、平移什么的复杂度直接爆炸,我们可以考虑将所有相同的数分到一组,然后对每组进行操作,因为要分段,所以得知道每个数的下标,所以我们按大小关系,将下标进行分组,并且用树状数组维护开头有多少个数是完成排序的,这样我们就可以知道任意一轮操作当前数的序列中的下标了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m,a[N],b[N],tot,_;
int tr[N];
vector<int> g[N],ans;
void add(int x,int y){while(x<=n)tr[x]+=y,x+=x&-x;}
int ask(int x){int res=0;while(x)res+=tr[x],x-=x&-x;return res;};
bool check()
{
    for(int i=1;i<n;i++)if(a[i]>a[i+1])return 0;
    return 1;
}
void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i],b[i]=a[i];
    if(check())
    {
        cout<<'0';
        if(_)cout<<"\n";
        return ;
    }
    if(m==1)cout<<-2;
    else if(m==2)
    {
        int pre=1;
        while(pre<n&&a[pre]<=a[pre+1])pre++;
        int suf=pre+1;
        while(suf<n&&a[suf]<=a[suf+1])suf++;
        if(suf==n&&a[n]<=a[1])
        {
            cout<<"1\n"<<"2\n";
            cout<<0<<" "<<pre<<" "<<n<<"\n";
            cout<<"2 1";
        }
        else cout<<-2;
    }
    else
    {
        for(int i=0;i<=n;i++)tr[i]=0,g[i].clear();
        ans.clear();
        sort(b+1,b+1+n);
        tot=unique(b+1,b+1+n)-b-1;
        for(int i=1;i<=n;i++)
        {
            int pos=lower_bound(b+1,b+1+tot,a[i])-b;
            g[pos].push_back(i);
        }
        for(int i=tot;i;i--)
        {
            for(auto v:g[i])
            {
                int pos=v;
                pos+=ask(pos);//因为是从后往前更新的,新的编号就是之前的编号加上加到开头的个数
                if(pos==1)continue;
                ans.push_back(pos);
                add(v,-1);
                add(1,1);
            }
        }
        cout<<ans.size()<<"\n";
        int sz=ans.size();
        for(auto v:ans)
        {
            sz--;
            if(v==n)
            {
                cout<<"2\n"<<0<<" "<<v-1<<" "<<v<<"\n";
                cout<<"2 1";
            }
            else 
            {
                cout<<"3\n"<<0<<" "<<v-1<<" "<<v<<" "<<n<<"\n";
                cout<<"2 1 3";
            }
            if(sz)cout<<"\n";
        }
    }
    if(_)cout<<"\n";
        
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>_;
    while(_--)solve();
    return 0;
}

总结:思考问题的时候,不要总想这一下子把所有问题想出,把一个大的问题分成若干个小问题,一步一步思考。

  1. Leapfrog 贪心、dp、思维、问题划分

大意:给定 n 个跳蛙,每个跳蛙有个初始得分 a i a_i ai 以及若干个 ( b i , t i ) (b_i,t_i) (bi,ti),初始时候只有一个。每次可以任选一个跳蛙死亡,并按顺序进行完该跳蛙所有的的 ( b i , t i ) (b_i,t_i) (bi,ti) ,对于每个 ( b i , t i ) (b_i,t_i) (bi,ti) 是进行 t i t_i ti 操作,每次任选一个存活的跳蛙,将它的分值加 b i b_i bi ,并把在该跳蛙后面加上一个 ( b i , t i ) (b_i,t_i) (bi,ti)。问最终剩下的那个跳蛙的最大值。 ( 1 < = n < = 1 e 5 , 1 < = a i , b i < = 1 e 4 , t i = 2 或 3 ) (1<=n<=1e5, 1<=a_i,b_i<=1e4,t_i=2或3) (1<=n<=1e5,1<=ai,bi<=1e4,ti=23)

思路:通过读题我们发现,其实对于某个回合选择去死亡的跳蛙,对于它后面的 ( b i , t i ) (b_i,t_i) (bi

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值