“蔚来杯“2022牛客暑期多校训练营2

G. Link with Monotonic Subsequence(规律)

给出n,构造一个permutation,使得其最长上升子序列和最长下降子序列的最大值最小。

思路: 从比较特殊的情况考虑,当我们把一个数组类似1,2,3,4,5,6,7两两互换位置后,很容易得到其最长下降子序列为2,最长上升子序列是互换的组数。我们要使得这两个数尽可能的接近,这样才能尽可能最小化两数的最大值。考虑每\left \lceil \sqrt{n} \right \rceil个数分为一组并倒置,例如3,2,1,6,5,4,7,这样我们一起考虑到每组分的个数和组数两个因素,并使其尽可能缩小差值,最长上升子序列是分成的组数,最长下降子序列是每组的个数。

AC Code:

#include <bits/stdc++.h>

const int N=1e6+5;
int t,n;
int a[N];

int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);
    std::cin>>t;
    while(t--){
        std::cin>>n;
        memset(a,0,sizeof(a));
        for(int i=1;i<=n;i++){
            a[i]=i;
        }
        int pos;
        for(int i=1;i<=n;i++){
            if((ll)i*(ll)i>=n){
                pos=i;
                break;
            }
        }
        for(int i=1;i*pos<=n;i++){
            for(int j=i*pos;j>=(i-1)*pos+1;j--){
                std::cout<<j<<' ';
            }
        }
        if(n%pos){
            int res=n%pos;
            for(int i=n;i>=n-res+1;i--){
                std::cout<<i<<' ';
            }
        }
        std::cout<<'\n';
    }
    return 0;
}

os:签到都好难想,唉

 J. Link with Arithmetic Progression(最小二乘法)

给定一个数列a,将其修改为一个等差数列b,代价为\sum_{i=1}^{n}(a_{i}-b_{i})^{2},最小化代价。

思路:看到这个等差数列,是一次函数的模型,且最小化代价,看到这个最小代价,联想到:

 直接带入公式计算残差平方和即可。

AC Code:

#include <bits/stdc++.h>

const int N=1e5+5;
int t,n;
int a[N];

int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        double ax=0,ay=0,tmpx=0,tmpy=0;
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            ax+=i,ay+=a[i];
        }
        ax/=(double)n,ay/=(double)n;
        for(int i=1;i<=n;i++){
            tmpx+=(i-ax)*(i-ax);
            tmpy+=(i-ax)*(a[i]-ay);
        }
        double bb=tmpy/tmpx;
        double aa=ay-bb*ax;
        double ans=0;
        for(int i=1;i<=n;i++){
            ans+=(bb*i+aa-a[i])*(bb*i+aa-a[i]);
        }
        printf("%.15lf\n",ans);
    }
    return 0;
}

os:这波是队友带飞了,队友一下就看出来线性回归方程,我高中的东西都忘干净了hhh

 K. Link with Bracket Sequence I(DP)

给出长度为n的字符串s,这个字符串是由长度为m的合法字符串删去若干括号得到的,求合法的字符串有多少。

思路:学习大佬的思路考虑三位DP数组,f[i][j][k]代表字符串长度为i,匹配长度为j,左括号比右括号多的数目为k时的方案数。左括号时,可以任意匹配;当是右括号时,只有当左括号数目大于右括号数目时才可以匹配。

AC Code:

#include <bits/stdc++.h>

const int N=105;
const int mod=1e9+7;
int t,n,m;
std::string s;
int f[N][N][N];

int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);
    std::cin>>t;
    while(t--){
        std::cin>>n>>m>>s;
        s=' '+s;
        memset(f,0,sizeof(f));
        f[0][0][0]=1;
        for(int i=0;i<m;i++){
            for(int j=0;j<=n;j++){
                for(int k=0;k<=i;k++){
                    f[i+1][j+(s[j+1]=='(')][k+1]=(f[i+1][j+(s[j+1]=='(')][k+1]+f[i][j][k])%mod;
                    if(k)
                        f[i+1][j+(s[j+1]==')')][k-1]=(f[i+1][j+(s[j+1]==')')][k-1]+f[i][j][k])%mod;
                }
            }
        }
        std::cout<<f[m][n][0]<<'\n';
    }
    return 0;
}

解释:循环i枚举字符串长度,循环j枚举匹配成功的长度,循环k枚举左括号比右括号多的数量;如果是左括号时,可以直接匹配,字符串长度+1,左括号比右括号多的数目+1;仅当左括号数目大于右括号时,才进行右括号匹配,字符串长度+1,左括号比右括号多的数目-1。

os:完了,不仅是简单DP搞不出来,这种要自己选择每维表示什么更难了啊

D. Link with Game Glitch(判负环)

有n种物品,m个关系,每个关系a,b,c,d表示可以用a个b类型的物品来制作c个d类型物品,现在要求找到一个最大的w,使得满足用a个b物品制作c*w个d物品,且不会出现制作出无限物品的情况。

思路:我们可以考虑建立b与d之间的关系,即每个b物品可以制作c/a个d,二分出答案w,若存在一个环使得一个x可以制作出大于1的x,则不满足条件,w需要小一点。

特殊处理:因为c/a大部分都是小于1的,所以很多个相乘很容易等于0,我们可以取对数,使得小于1的数,都变成较大的负数,利用log(x*y)=log(x)+log(y),这样解决了精度问题,而对于原问题判断是否大于1的问题,也转化为是否大于0的问题。

注:因为是带负权图,处理可以采用Bellman-Ford,注意更新w条件是不出现一个正环,这样才可以保证不会无限制造物品。浮点数二分时直接自定义一个循环次数,不要用while(l<r)了。

AC Code:

#include <bits/stdc++.h>

const int N=1050;
int n,m,a,b,c,d;
double dis[N];

struct node{
    int u,v;
    double w;
};

std::vector<node>vec;

bool check(double k){
    for(int i=1;i<=n;i++) dis[i]=0;
    k=log(k);
    int cnt=0;
    while(1){
        bool flag=true;
        for(auto it:vec){
            int u=it.u,v=it.v;
            double w=it.w;
            if(dis[u]+k+w>dis[v]){
                dis[v]=dis[u]+k+w;
                flag=false;
            }
        }
        if(flag) break;
        cnt++;
        if(cnt>n+10) return false;
    }
    return true;
}

int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);
    std::cin>>n>>m;
    for(int i=1;i<=m;i++){
        std::cin>>a>>b>>c>>d;
        double w=(double)c/(double)a;
        vec.push_back({b,d,log(w)});
    }
    double l=0,r=1;
    double ans=0;
    for(int i=1;i<=100;i++){
        double mid=(l+r)/2;
        if(check(mid)){
            ans=std::max(ans,mid);
            l=mid;
        }
        else r=mid;
    }
    std::cout<<std::fixed<<std::setprecision(10)<<ans<<'\n';
    return 0;
}

os:才发现原来cin cout也可以确定浮点数精度   std::fixed<<std::setprecision(10) ,长芝士了。

L. Link with Level Editor I(滚动数组)

 有n个世界,每个世界都有m个点,在第i个世界中,最多可以选择一条边u->v,从u移动到v(可以选择不移动)。随后进入到第i+1个世界中的点v。如果选择在u点上不移动,则进入第i+1个世界的点u,现要求找到一段连续的世界[L,R],使得可以从点1到点m,最小化这个区间。

思路:学习大佬的思路对于第i个世界,我们枚举这个世界中的边u->v,想要到达v有两种方式,从i-1世界中的v到达,从i-1世界中的u到达i世界的u,再到v。因为我们只需要考虑1到m穿越世界的个数,随意我们记录到每个点u最近的点1的世界编号。

采用滚动数组pre表示i-1的世界,now为当前i世界,若存在u->v且v=m,那么我们用min(ans,i-now[v]+1)来更新穿越世界的最小值。注意now[v]为最近的点1所在的世界。那么对于u->v还需要更新now[v]的值,now[v]=max(now[v],pre[u])。

AC Code:

#include <bits/stdc++.h>

#define INF 0x3f3f3f3f
const int N=2e5+5;
int n,m;

signed main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);
    std::cin>>n>>m;
    std::vector<int>pre(m+1,-INF),now(m+1,-INF);
    int ans=INF;
    for(int i=1;i<=n;i++){
        int l;
        std::cin>>l;
        pre[1]=i;
        for(int j=1;j<=l;j++){
            int u,v;
            std::cin>>u>>v;
            now[v]=std::max(now[v],pre[u]);
            if(v==m) ans=std::min(ans,i-now[v]+1);
        }
        pre=now;
    }
    if(ans>n) std::cout<<-1<<'\n';
    else std::cout<<ans<<'\n';
    return 0;
}

H. Take the Elevator(模拟)

n个人坐电梯,楼高k,每个人有起始楼层和目标楼层,电梯有载客量限制m,可以上升至任意层下降,但是下降一定要到第一层后再上升,电梯每秒运行一层,换方向和上下人不占用时间,问电梯最短运行时间。

大佬的思路

思路:把所有人分两类考虑,上行的一类,下降的一类,两者是相同的。对于电梯的一次上行,首先取出需要到达最高的,同时维护一个电梯中人员序列,仅记录其上电梯的时刻,当序列长度小于m时,继续取到达位置高的,若电梯已满,只能选择进电梯楼层最高的还没上之前就把人运到,因而选择最大的、同时下电梯位置不超过序列中最大的进电梯的人加入电梯,这样维护一次电梯能带走多少人,记录最高楼层。上行下行倒道理一样,一次运行必定满足一次上行和一次下行,做归并即可。

AC Code:

#include <bits/stdc++.h>

typedef long long ll;
typedef std::pair<ll,ll>PLL;
#define INF 0x3f3f3f3f
const int N=2e5+5;
int n,m,k;

std::vector<ll> cal(std::vector<PLL> &que){
    std::sort(que.begin(),que.end());
    std::multiset<ll>s;
    for(auto i:que){
        auto it=s.upper_bound(i.first);
        if(it!=s.begin()) s.erase(prev(it));
        s.insert(i.second);
    }
    std::vector<ll>ans;
    while(!s.empty()){
        int times=m;
        ans.push_back(*s.rbegin());
        while(!s.empty()&&times--)
            s.erase(s.find(*s.rbegin()));
    }
    return ans;
}

int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);
    std::cin>>n>>m>>k;
    std::vector<PLL>up,down;
    for(int i=1;i<=n;i++){
        int a,b;
        std::cin>>a>>b;
        if(a<b) up.push_back({a,b});
        else down.push_back({b,a});
    }
    auto f1=cal(up),f2=cal(down);
    ll ans=0;
    int len1=f1.size(),len2=f2.size();
    for(int i=0;i<std::max(len1,len2);i++){
        ll d1=i<f1.size()?f1[i]:0;
        ll d2=i<f2.size()?f2[i]:0;
        ans+=2*(std::max(d1,d2)-1);
    }
    std::cout<<ans<<'\n';
    return 0;
}

os:难度较大的贪心

 C. Link with Nim Game(Nim博弈)

 有n堆石头,A和B轮流进行操作,A先手,每次操作可以从任意一堆石头中取正整数个石头,若无法完成操作则输,在双方都采取最优策略情况下,必败一方想尽可能慢的结束游戏,必胜的一方想尽可能快的结束游戏,求游戏结束时进行的操作和第一轮可能进行的操作种类数。

大佬的思路

思路:首先判断胜负是Nim游戏的结论,若所有n堆石子数异或和不为0时,先手必胜,反之后手必胜。分类讨论,若先手必胜则第一次先手会拿最多的石头;若先手必败,则二人必定一个一个拿,对于方案数需要判断拿一个石头后下一步是否必须也拿一个石头。

先看先手必胜时,因为希望尽快结束比赛,那么就要找到能取出的最大值,取出后使得剩余石头堆的异或和为0,对于最大值的计算,若假设对a[i]取,剩下x,即sum^a[i]^x=0,那么a[i]-x即最大值;对于先手必败时,输的人必须一个一个拿,但是不能让对手有拿若干石头也能让异或和变为0的情况,也就是说我们需要在某一堆拿掉一个石头,记录每一堆拿掉一个石头剩下的异或和放入set去重,再转化为刚才分析必胜态的状态,找到使得异或和为0的值,若取出的值可以大于1,那么对手存在拿多个石头而加速游戏结束的状态,因此我们只需要找到哪些方案拿完一个石头接下来必拿一个石头的种类即可。

AC Code:

#include <bits/stdc++.h>

typedef long long ll;
typedef std::pair<ll,ll>PLL;
#define int long long
#define INF 0x3f3f3f3f
const int N=2e5+5;
int t,n;
std::vector<int>a(N,0);

signed main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);
    std::cin>>t;
    while(t--){
        std::cin>>n;
        int sum=0,tot=0;
        for(int i=1;i<=n;i++){
            std::cin>>a[i];
            sum^=a[i],tot+=a[i];
        }
        if(sum){
            int max=0,ans=0;
            for(int i=1;i<=n;i++){
                int x=a[i]-(sum^a[i]);
                max=std::max(max,x);
            }
            for(int i=1;i<=n;i++){
                int x=a[i]-(sum^a[i]);
                if(x==max) ans++;
            }
            std::cout<<tot-max+1<<' '<<ans<<'\n';
        }
        else{
            std::set<int>S,T;
            for(int i=1;i<=n;i++){
                int x=sum^a[i]^(a[i]-1);
                S.insert(x);
            }
            for(auto u:S){
                for(int i=1;i<=n;i++){
                    int x=sum^a[i]^u;
                    if(a[i]-x>1) T.insert(u);
                }
            }
            for(auto u:T) S.erase(u);
            int ans=0;
            for(int i=1;i<=n;i++)
                if(S.count(a[i]^(a[i]-1)))
                    ans++;
            std::cout<<tot<<' '<<ans<<'\n';
        }
    }
    return 0;
}

补不动了。。。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值