noi online round2(入门组)

又来了,第一题还是比较简单的,而且正好前面在弄最长不下降子序列的时候学到了二分的函数,lower_bound()和upper_bound的知识,刚好用上了,但是后面两个题目,第二题稍微写了下,但是越写越觉得不对。。第三题就一直放着,趁着写博客又来攻克一下吧~┭┮﹏┭┮脑壳不够用


 

题目一:末了(真的挺简单的,我这种菜鸡还是能做出来心里还是很开心,不是零分了嘤)简单思路就是贪心加二分,没什么好说的,看代码应该就可以看懂,也可以比距离

#include <bits/stdc++.h>
using namespace std;
const int N=200050,inf=0x3f3f3f;
int a[N];
double t[N],ask;
int n,l,v,q;
bool cmp(int x,int y) {return x>y;}
int main()
{
    cin>>n>>l>>v;
    for(int i=1;i<=n;i++) cin>>a[i];
    sort(a+1,a+1+n,cmp);
    t[0]=l*1.0/(v*1.0);
    for(int i=1;i<=n;i++)
        t[i]=t[i-1]+a[i]*1.0/(v*1.0);
    cin>>q;
    for(int i=1;i<=q;i++){
        cin>>ask;
        if(ask>=t[n]){
            cout<<"-1"<<endl;continue;
        }
        cout<<upper_bound(t,t+n,ask)-t<<endl;
    }
    return 0;
}

题目二:荆轲刺秦王

 先来学习一下差分:

从一个例子开始:将2~5区间内的每个数加上6

原数组                    5 2 0 1 3 1 4

修改后的数组          5 8 6 7 9 1 4

原差分数组              5 -3 -2 1 2 -2 3

修改后的差分数组   5 3 -2 1 2 -8 3

我们可以发现,只有头和尾的差分会发生变化,并且变化规律为  a[l]+=k,(-3+6=3)  a[r+1]-=k;(-2-6=-8)

可能对于差分的理解不够,还没有找到我特别能理解的差分的典型例子,找到了再来补一补吧

但是大概能看懂代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,c1,c2,d;//如题
int sx,sy,ex,ey;//起点终点坐标
string s;//读入的数据
int a[351][351];
int flag[351][351];
bool v[351][351][16][16];//剪枝二
void add(int i,int j,int x){
    for(int k=-x+1;k<=x-1;k++){//差分使复杂度降为n^3
        if(k+i<1||k+i>n)continue;//超过边界 
        int p=x-1-(k<0?-k:k);//可以自己找规律
        if(j-p<1)a[k+i][0]+=1;
        else a[k+i][j-p]+=1;
        if(j+p+1>m);
        else a[k+i][j+p+1]-=1;
    }
}
struct zj{
    int x,y,u1,u2,t;//坐标,隐身使用次数,瞬移使用次数,已经过了多长时间
};
int ans1=0x3fffffff,ans2=0x3fffffff,ans=0x3fffffff;
int X[8]={0,0,1,-1,1,1,-1,-1};
int Y[8]={1,-1,0,0,1,-1,1,-1};
void bfs(){
    queue<zj> q;
    q.push((zj){sx,sy,0,0,0});
    v[sx][sy][0][0]=1;//起点已经过
    while(!q.empty()){
        zj x=q.front();
        q.pop();
        if(x.t>ans)continue;//剪枝一
        if(x.x==ex&&x.y==ey){
            if(x.t<ans){
                ans=x.t;
                ans1=x.u1;
                ans2=x.u2;
            }
            else{
                if(ans1+ans2>x.u1+x.u2){//魔法使用次数少
                    ans=x.t;
                    ans1=x.u1;
                    ans2=x.u2;
                }
                else if(ans1+ans2==x.u1+x.u2&&ans1>x.u1){//魔法一样,隐身少
                    ans=x.t;
                    ans1=x.u1;
                    ans2=x.u2;                    
                }
            }
            continue;
        }
        for(int i=0;i<8;i++){
            int xx=x.x+X[i],yy=x.y+Y[i];
            if(xx<1||xx>n||yy<1||yy>m)continue;//越界
            if(flag[xx][yy]==1)continue;//有士兵
            if(a[xx][yy]<=0&&v[xx][yy][x.u1][x.u2]==0){//不在士兵的观察范围内
                v[xx][yy][x.u1][x.u2]=1;//标记
                q.push((zj){xx,yy,x.u1,x.u2,x.t+1});
            }
            else if(x.u1+1<=c1&&v[xx][yy][x.u1+1][x.u2]==0){//在士兵的观察范围内,使用隐身
                v[xx][yy][x.u1+1][x.u2]=1;//标记
                q.push((zj){xx,yy,x.u1+1,x.u2,x.t+1});
            }
        }
        if(x.u2+1>c2)continue;//无法使用瞬移
        for(int i=0;i<4;i++){
            int xx=x.x+X[i]*d,yy=x.y+Y[i]*d;
            if(xx<1||xx>n||yy<1||yy>m)continue;
            if(flag[xx][yy]==1)continue;
            if(a[xx][yy]<=0&&v[xx][yy][x.u1][x.u2+1]==0){
                v[xx][yy][x.u1][x.u2+1]=1;
                q.push((zj){xx,yy,x.u1,x.u2+1,x.t+1});
            }
            else if(x.u1<c1&&v[xx][yy][x.u1+1][x.u2+1]==0){
                v[xx][yy][x.u1+1][x.u2+1]=1;
                q.push((zj){xx,yy,x.u1+1,x.u2+1,x.t+1});
            }
        }
    }
}
int main(){
    scanf("%d%d%d%d%d",&n,&m,&c1,&c2,&d);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>s;
            if(s=="S")flag[i][j]=-2,sx=i,sy=j;//记录起点位置 
            else if(s=="T")flag[i][j]=-1,ex=i,ey=j;//记录终点位置 
            else if(s==".");
            else{
                flag[i][j]=1;
                int x=s[0]-'0';
                for(int i=1;i<s.length();i++)x=x*10+s[i]-'0';//存入卫兵可以观察的距离 
                add(i,j,x);//处理 
            }
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            a[i][j]+=a[i][j-1];//注意差分要加回去
        }
    }
    bfs();
    if(ans==0x3fffffff)printf("-1");//无解
    else printf("%d %d %d",ans,ans1,ans2);//输出
    return 0;
}
View Code

 为什么用自己当时的做法结合一下只能得八十分呢~好奇怪,最容易错的反而没有错:

#include <bits/stdc++.h>
using namespace std;
const int N=405,inf=0x3f3f3f3f;
int n,m,c1,c2,s,a[N][N],sol[N][N],vis[N][N][20][20];
int d[8][2]={{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}};
int sx,sy,ex,ey,ans=inf,ans1=inf,ans2=inf;
struct node{
    int x,y,c1,c2,num;
};
void pre(int x,int y,int dis,int step)//标记士兵范围的区域 
{
    int rx,ry;
    if(step==dis) return;
    else{
        for(int i=0;i<4;i++){
            rx=x+d[i][0];
            ry=y+d[i][1];
            if(!sol[rx][ry]&&!a[rx][ry]&&rx>=1&&rx<=n&&ry>=1&&ry<=m){
                a[rx][ry]=1;//表示在士兵范围内 
                pre(rx,ry,dis,step+1);
            }
        }
    }
}
int num(string s)
{
    int ans=0;
    for(int i=0;i<s.length();i++)
        ans=ans*10+s[i]-'0';
    return ans;
}
void bfs()
{
    queue<node> q;
    q.push((node){sx,sy,0,0,0});
    vis[sx][sy][0][0]=1;
    while(!q.empty())
    {
        node p=q.front();q.pop();
        if(p.num>ans) continue;//剪枝
        if(p.x==ex&&p.y==ey){
            if(p.num<ans){ans=p.num;ans1=p.c1;ans2=p.c2;}
            else{
                if(p.c1+p.c2<ans1+ans2){ans=p.num;ans1=p.c1;ans2=p.c2;}
                else if(p.c1+p.c2==ans1+ans2&&ans1>p.c1){ans=p.num;ans1=p.c1;ans2=p.c2;}
            }
            continue;
        }
        for(int i=0;i<8;i++){
            int rx=p.x+d[i][0],ry=p.y+d[i][1];
            if(rx<1||rx>n||ry<1||ry>m) continue;//越界
            if(sol[rx][ry]>0) continue;//有士兵
            if(!a[rx][ry]&&!vis[rx][ry][p.c1][p.c2]){//不在士兵的观察范围内并且没有访问过 
                vis[rx][ry][p.c1][p.c2]=1;//标记
                q.push((node){rx,ry,p.c1,p.c2,p.num+1});
            }
            else if(p.c1+1<=c1&&!vis[rx][ry][p.c1+1][p.c2]){//在士兵的观察范围内,使用隐身
                vis[rx][ry][p.c1+1][p.c2]=1;//标记
                q.push((node){rx,ry,p.c1+1,p.c2,p.num+1});
            }
        }//只使用隐身或者直接走的情况
        if(p.c2+1>c2) continue;//已经不能使用瞬移就直接结束
        for(int i=0;i<4;i++) 
        {
            int rx=p.x+d[i][0]*s,ry=p.y+d[i][1]*s;
            if(rx<1||rx>n||ry<1||ry>m) continue;//越界
            if(sol[rx][ry]>0) continue;//有士兵
            if(!a[rx][ry]&&!vis[rx][ry][p.c1][p.c2+1]){//不在士兵的观察范围内并且没有访问过 
                vis[rx][ry][p.c1][p.c2+1]=1;//标记
                q.push((node){rx,ry,p.c1,p.c2+1,p.num+1});
            }
            else if(p.c1+1<=c1&&!vis[rx][ry][p.c1+1][p.c2+1]){//在士兵的观察范围内,额外使用隐身
                vis[rx][ry][p.c1+1][p.c2+1]=1;//标记
                q.push((node){rx,ry,p.c1+1,p.c2+1,p.num+1});
            }
        }
    }
}
int main()
{
    //cin>>n>>m;
    //pre(3,3,3,1); 
    scanf("%d %d %d %d %d",&n,&m,&c1,&c2,&s);
    string c;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            cin>>c;
            if(c[0]=='S') {sol[i][j]=-1;sx=i;sy=j;}
            else if(c[0]=='T'){sol[i][j]=-2;ex=i;ey=j;}
            else if(c[0]>='0'&&c[0]<='9'){
                sol[i][j]=num(c);
                pre(i,j,sol[i][j],1);
            } 
        }
    bfs();
    
    if(ans==inf)printf("-1");//无解
    else printf("%d %d %d",ans,ans1,ans2);//输出
    /*
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
            cout<<a[i][j]<<" ";
        cout<<endl;
    }
    cout<<endl;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
            cout<<sol[i][j]<<" ";
        cout<<endl;
    }
    */
    //dfs(sx,sy,0,0,0);
    return 0;
}
View Code

 来了,找到原因了,补充来了:

用dfs或者bfs处理曼哈顿距离(正确做法75分(tle) 错误做法反而能得80分(wa))

处理最短时间的代码都是一样的,唯一不同的就是处理曼哈顿距离这里:

 dfs:

void pre(int x,int y,int dis,int step)//标记士兵范围的区域 
{
    int rx,ry;
    if(step==dis) return;
    for(int i=0;i<4;i++){
        rx=x+d[i][0];
        ry=y+d[i][1];
        if(sol[rx][ry]<=0&&rx>=1&&rx<=n&&ry>=1&&ry<=m){//错误做法就是加上一个条件!a[rx][ry] 能得80分 
            a[rx][ry]=1;//表示在士兵范围内 
            pre(rx,ry,dis,step+1);
        }
    }
}

bfs:

void pre(int x,int y,int dis,int step) 
{
    queue<obs> q;int rx,ry;//还有可能超空间 
    q.push((obs){x,y,step});
    while(!q.empty())
    {
        obs p=q.front();q.pop();
        if(p.step==dis) continue;
        for(int i=0;i<4;i++){
            rx=p.x+d[i][0];
            ry=p.y+d[i][1];
            if(sol[rx][ry]<=0&&rx>=1&&rx<=n&&ry>=1&&ry<=m){
                a[rx][ry]=1;//表示在士兵范围内 
                q.push((obs){rx,ry,p.step+1});
            }
        }
    }
}

 

题目三:建设城市(分为四段,1-i i+1-n n+1-j j+1-2n

dp[i][j]:长度为i,末尾为j的单调递增的序列个数

dp[i][j]=Σk=1jdp[i-1][k];

优化:也可以状态转移为:dp[i][j]表示长度为i,末尾<=j的单调递增序列,d[i][j]=dp[i][j-1]+dp[i-1][j]   60分

继续优化:排列组合的插板?~dp[i][j]=C(i+j-1,i)=(i+j-1)!/i!(j-1)!

线性求阶乘逆元优化    100分

好吧:又来学习一下做这个题目应该掌握的预备知识了

来了,第一步:如果没有第5个要求的话,那么,我们只来看一看左边上升部分有多少种情况呢?我们有n栋楼,所有楼的高度范围为(1-m),那么我们可以把这个模型抽象为有n个球,现在把它们放入m个盒子中(编号就为1-m,在在哪个盒子就高度是多少,可以相同),所以我们就是允许有些盒子为空的,那么我们就根据插板法:

总结:

1:如果是把n个求放入k个盒子中(每个盒子必须要有球),那么由插板法得 方案数为 C(n-1,k-1);

2:如果是把n个求放入k个盒子中(盒子可以为空),那么由插板法得 方案数为 C(n+k-1,k-1);我们很显然是这个情况~(get it)

 第二步:那么我们现在要把第5个要求加进来的话,现在就有两种情况了,x/y在两侧还是同侧,我们先枚举这两栋楼高度为i

1、在两侧的话:

x左边的x栋楼高度范围为(1-i);x右边到n左边(包括n)n-x栋楼的高度范围为(i,m);

n右边(不包括n)到y左边y-n-1栋楼的高度范围为(i,m);y右边的2n-y栋楼的高度范围为(1-i)

就把四种情况乘起来就是答案了

2、如果在一侧的话:就把x,y之间的高楼看成一个高楼

就有c(n+m-1,m-1)*c(n+x-y+m-1,m-1);

又有问题来了:我们知道c(n+m-1,m-1)=(n+m-1)!/(m-1)!*n!

就有乘法的逆元:

(a+b)%p=(a%p+b%p)%p

(a-b)%p=(a%p-b%p)%p

(a*b)%p=(a%p*b%p)%p

但是(a/b)%p!=(a%p/b%p)%p;除法不满足我们的分配律,但我们也需要在这个过程中去模,否则中间值会出现太大的情况,所以我们要用到乘法逆元:

乘法逆元一般用于求a/b(mod)p的值,是解决模意义下分数值的必要手段。

逆元定义:若a*x=1(mod b),且a与b 互斥,那么我们就能定义x为a的逆元,记为a-1,所以我们也可以称x为a在mod意义下的倒数。所以对于a/b(mod p) ,我们就可以求出 b 在mod p 下的逆元,然后乘上 a ,再 mod p就是这个分数的值了。。。没太看懂,举例,5和14是互斥的,所以就存在5关于模14的乘法逆元为3,3*5-14=1;

可以看成一个公式就是ax-pb=1,把减号变成加号,所以有一个公式就变成:ax+by=1;,然后可以扩展欧几里得定理来扩展了。

应用(求取(a/b)%p等同于求取a*(b的逆元)%p)

证明:

 那么问题又来了,怎么去求解乘法的逆元是多少呢?方法很多种:费马小定理(p为质数)、扩展欧几里得、线性递推......

费马小定理:假如a是一个整数,p是一个质数,那么

1、如果a是p的倍数 a^p=a(mod p)

2、如果a不是p的倍数,a^(p-1)=1(mod p)    乘法逆元中要求互斥,所以肯定不是倍数

同余式:a=b(mod n)表示a和b对模n同余,即正整数a-b能被n整除

所以   a*a^(p-2)=1 (mod p)那么a^(p-2) 就是 a 的逆元了~

但是需要注意:上面都不是等号,有(mod p)的这种都是同余符号,三个横线;所以这个式子完整的应该是(a%p)*(a^(p-2))%p=1%p,这才是等式

例如:a=5,p=3;那么a的逆元就是 5^(3-2)%3=2;,所以在这里就可以也可以用快速幂进行优化;

ll fpm(ll x,ll power,ll mod)//x^power%mod就是要求的逆元 
{
    x%=mod;
    ll ans=1;
    for(;power;power>>=1,(x*=x)%=mod)
        if(power&1) (ans*=x)%=mod;
    return ans; 
}

****求一串数字的逆元可以用线性算法,代码过背吧...

ll inv[maxn]={0,1};
int main(){
    int n,p;
    scanf("%d%d",&n,&p);
    printf("1\n");
    for(int i=2;i<=n;i++)
        inv[i]=(ll)p-(p/i)*inv[p%i]%p,printf("%d\n",inv[i]);
    return 0;
}

 

那么整个题目的分析应该就完了

最后再来总结一下这个题目,

首先我们清楚了n栋楼房在高度范围为<=m的所有排列情况,因为可以为空所以一共有c(n+m-1,m-1)种方案;

然后我们加上限制条件5,两种情况:一种四段相乘,一种两段相乘 

然后我们知道c(m+n-1,m-1)=(m+n-1)!/n!*(m-1)!,由于数会很大,所以我们需要模p,但是除法不满足分配律,我们利用乘法逆元把它变成乘法的模,就可以防止中间数会很大溢出;

我们知道要用乘法逆元之后,选择求出逆元的方法有很多,这里用费马小定理,那么我们在求的时候还可以加上快速幂,那么关于

over!下面就来看代码了~(好吧,尽量理解了)

 !!!!!!!!我感觉思路没有什么问题,但答案就是不对。。。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=998244353;
ll ans=0;
int n,m,x,y;
ll fpm(int num,int power)//x^power%mod就是要求的逆元 
{
    num%=mod;
    ll res=1;
    for(;power;power>>=1,(num *= num) %=mod)
        if(power&1) (res *= num) %=mod;
    /*应该就是快速幂 
    for(;power;power>>=1,(x*=x)%=mod)
        if(power&1) (ans*=x)%=mod;
    */
    return res; 
}
ll c(int a,int b)
{
    ll sum=1;
    for(int i=1;i<=b;i++)
    {
        sum=sum%mod*(a-b+i)%mod;
        sum=sum%mod*fpm(i,mod-2)%mod;
    } 
    return sum;
}
int main()
{
    cin>>m>>n>>x>>y;
    if(x<=n&&y>=n)//在两侧的情况
    {
        for(int i=1;i<=m;i++)//枚举相等的两栋楼的情况
            //ans=(ans+((ll)f(x-1,i)*f(n-x,m-i+1)%mod*f(y-n-1,m-i+1)%mod*f(n*2-y,i)%mod))%mod;
            ans+=c(x+i-1,i-1)%mod*c(n-x+m-i,m-i)%mod*c(y-n+m-i,m-i)%mod*c(2*n-y+i-1,i-1)%mod;
    } 
    else 
    {
        //ans=(ll)c(n+m,m)*c(x+n-y+m,m)%mod;
        ans=c(n+m-1,m-1)%mod*c(n+m-1-x+y,m-1)%mod;
    }
    cout<<ans<<endl;
    return 0;
}
有问题

 

 

有人知道就评论一下吧,所以现在换一个做法,其实思路差不多就是划分的区域和代码有点出入。

划分区域:1x1,x+1n,n+1y1,y+1n2

那么就是f(i,j)=(i+j-1)!/(j-1)!*i!

所以先进行预处理求出相关的逆元与阶乘,所以就可以用到线性逆元的方法,前面也写了,直接记住也可以:

#include<bits/stdc++.h>
#define ll long long
#define mod 998244353
using namespace std;
const int N=200001;
int n,m,x,y;
ll k[N],inv[N],invk[N];
ll ans=0;
ll f(ll a,ll b)
{
    return (ll)(k[a+b-1]*invk[a]%mod*invk[b-1]%mod);
}

int main(){
    scanf("%d%d%d%d",&m,&n,&x,&y);
    k[0]=inv[1]=invk[0]=1;
    for(int i=2;i<=n+m;i++) inv[i]=((ll)mod-mod/i)*inv[mod%i]%mod;//处理1到n的逆元
    for(int i=1;i<=n+m;i++) k[i]=(ll)k[i-1]*i%mod;//处理阶乘
    for(int i=1;i<=n+m;i++) invk[i]=(ll)invk[i-1]*inv[i]%mod;//处理阶乘数组的逆元
    if(x<=n&&y>n){
        for(int i=1;i<=m;i++) ans=(ans+(ll)(f(x-1,i)*f(n-x,m-i+1)%mod*f(y-n-1,m-i+1)%mod*f(n*2-y,i)%mod))%mod;
        printf("%lld",ans);
    }
    else{
        printf("%lld",(ll)f(n,m)*f(x+n-y,m)%mod);
    }
    return 0;
}
View Code

 在来简单版的讲解:

一道关于排列组合的数学题

首先有五个条件,先不看第五个条件,没有x,y高度相等这个条件

那么就直接看n座楼,他们的高度范围是[1-m],抽象为n个球放入到m个盒子中

数学中是这样来做的:

1:如果是把n个球放入k个盒子中(每个盒子必须要有球),那么由插板法得方案数为 C(n-1,k-1);

2:如果是把n个球放入k个盒子中(盒子可以为空),那么由插板法得 方案数为 C(n+k-1,k-1);

这个题目显然就是第二种情况;

现在又加上条件五的话,我们就可以把这个分成两种情况:

1、x,y在同侧

那么一侧为 n个球 m个盒子 我们写成f(n,m)

有x,y在的一侧,x-y的高度是相同的,我们把这一段看成一个整体 就相当于f(n+y-x,m)

2、x,y在两侧

就分成四段 1-x-1 x+1-n n+1-y-1 y+1-2*n

去枚举x,y的高度i

这四段就相当于 f(x-1,i) f(n-x,m-i+1) f(y-n-1,m-i+1) f(n*2-y,i)

答案就是他们相乘

但是f(i,j)=c(i+j-1,j-1)=(i+j-1)!/(j-1)!i!

除法不满足分配率,不能取余,但是中间数可能会很大,所以就要用到乘法逆元

证明过程省去,知道两点:

1、(a/b) %p 等于 (a*b的逆元)%p

2、费马小定理得出 b的逆元为 b^p-2 (快速幂)

#include<bits/stdc++.h>
#define ll long long
#define mod 998244353
using namespace std;
const int N=200001;
int n,m,x,y;
ll k[N],inv[N],invk[N];
ll ans=0;
ll f(ll a,ll b)
{
    return (ll)(k[a+b-1]*invk[a]%mod*invk[b-1]%mod);
}
ll quickpow(ll a,int b){//快速幂写法一 

    if(b==1) return a;
    ll res=(ll)quickpow(a,b>>1);
    if(b%2==1) return (ll)res*res%mod*a%mod;
    else return (ll)res*res%mod;
}
/* 
ll quickpow(ll a,int b){//快速幂写法二 
    ll res=1;
    while(b)
    {
        if(b&1) res=(ll)res*a%mod;
        a=(ll)a*a%mod;
        b>>=1;
    }
    return res;
}*/
int main(){
    scanf("%d%d%d%d",&m,&n,&x,&y);
    k[0]=invk[0]=1;
    for(int i=1;i<=n+m;i++) k[i]=(ll)k[i-1]*i%mod;//处理阶乘
    for(int i=1;i<=n+m;i++) invk[i]=(ll)quickpow(k[i],mod-2);//处理阶乘数组的逆元
    if(x<=n&&y>n){
        for(int i=1;i<=m;i++) ans=(ans+(ll)(f(x-1,i)*f(n-x,m-i+1)%mod*f(y-n-1,m-i+1)%mod*f(n*2-y,i)%mod))%mod;
        printf("%lld",ans);
    }
    else{
        printf("%lld",(ll)f(n,m)*f(x+n-y,m)%mod);
    }
    return 0;
}
View Code

 

好吧下一次的noi online 又要来了,慌张。。。

posted @ 2020-05-03 19:52  sumoier  阅读(163)  评论(0编辑  收藏  举报