2022牛客蔚来杯第二场 G J K L D C (H)

2022牛客蔚来杯第二场

G.Link with Monotonic Subsequence

  • 题意

​ 构造一个n的排列,使得排列的max( lis , lds )最小

  • 题解

    可以按照sqrt(n)来分块

    Eg: n=9

    排列:321 654 987

    其中lis与lds都为sqrt(n),其中lds的长度来自每个块内部,lis来自所有块之间。

    故:只需分块后数字倒叙输出就行

  • 代码

#include <iostream>
#include <cmath>
#include <algorithm>

using namespace std;

void solve() {
    int n;
    cin>>n;
    int sq=ceil(sqrt(n));//表示块数以及每个块的个数
    for(int i=1;i*sq<=n;i++) {//把完整的块输出
        int st=(i-1)*sq+1;//起点数值
        int ed=i*sq;//终点数值
        for(int j=ed;j>=st;j--) cout<<j<<" ";//倒叙输出
    }
    for(int i=n;i>(n/sq)*sq;i--) cout<<i<<" ";//不完整块的输出
  //(n/sq)为完整块的块数,(n/sq)*sq表示完整块数的最后一个数值
    cout<<'\n';
}

int main() {
    int T;
    cin>>T;
    while(T--) solve();
    
    return 0;
}

J.Link with Arithmetic Progression

  • 题意

    给定一个数组a,问将数组a变成等差数列b使得(bi-ai)*(bi-ai)之和最小

  • 题解

    线性回归定义:

    故:题目转化为,一堆点(i,ai),求直线(等差数列就是直线呐)b=Ax+B使得式子最小的变化参数A,B,最后求误差式子的值

  • 代码

#include <iostream>
#include <cstdio>

using namespace std;
typedef long long LL;
typedef double ld;
const int N=1e5+10;

int a[N];

void solve() {
    int n;
    scanf("%d",&n);
    
    ld x_average=0,y_average=0;//平均值
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
        y_average+=a[i];
        x_average+=i;
    }
    y_average/=n,x_average/=n;
    
    ld A=0,B=0;
    for(int i=1;i<=n;i++)
        A+=(i-x_average)*(a[i]-y_average),
        B+=(i-x_average)*(i-x_average);
    A/=B;
    B=y_average-A*x_average;//求得最优变换参数A,B
    
    ld ans=0;
    for(int i=1;i<=n;i++) 
        ans+=(A*i+B-a[i])*(A*i+B-a[i]);//差值答案
    printf("%.15lf\n",ans);
        
}

int main() {
    int T;
    scanf("%d",&T);
    while(T--) solve();
    
    return 0;
}

K.Link with Bracket Sequence I

  • 题意

    给定一个长度为n的括号序列a,求长度为m的合法括号序列b,同时满足a是b的子序列的序列b的数量

  • 题解

    • 找能够匹配子序列a的序列b,那么容易想到使用最长公共子序列,即动态规划。f[i,j]:=在b的前i位,匹配了a序列的前j位的序列数量
    • 但题目还有个条件要求序列b是合法的,我们可由前置知识得到以左括号+1,右括号-1,则最终合法序列值为0,同时期间不出现值<0。那么在动态数组中多加入一维k,来判定序列是否合法

    动态规划

    • 定义:f[i,j,k]指b序列前i位中匹配了a序列的前j位,同时b序列中的净左括号数量为k

    • 状态转移:

      b[i]放’(‘。(注意:k需要大于0,否则数组越界)

      1. a[j]=‘(’,即刚好两序列的最后一位匹配,那么数量在此情况下等于b序列前i-1位还需匹配a序列前j-1位同时有k-1个括号数量的序列数量
        f [ i , j , k ] + = f [ i − 1 , j − 1 , k − 1 ] f[i,j,k]+=f[i-1,j-1,k-1] f[i,j,k]+=f[i1,j1,k1]

      2. a[j]=‘)’,即b第i位与a第j位不匹配,那么此时数量等于b序列前i-1位需要匹配a序列的前j位,同时左括号数量为k-1的序列数量
        f [ i , j , k ] + = f [ i − 1 , j , k − 1 ] f[i,j,k]+=f[i-1,j,k-1] f[i,j,k]+=f[i1,j,k1]

      b[i]放’)’

      1. a[j]=‘(’,即b第i位与a第j位不匹配,那么此时数量等于b序列前i-1位需要匹配a序列的前j位,同时左括号数量为k+1的序列数量
        f [ i , j , k ] + = f [ i − 1 , j , k + 1 ] f[i,j,k]+=f[i-1,j,k+1] f[i,j,k]+=f[i1,j,k+1]

      2. a[j]=‘)’,即刚好两序列的最后一位匹配,那么数量在此情况下等于b序列前i-1位还需匹配a序列前j-1位同时有k+1个括号数量的序列数量
        f [ i , j , k ] + = f [ i − 1 , j − 1 , k + 1 ] f[i,j,k]+=f[i-1,j-1,k+1] f[i,j,k]+=f[i1,j1,k+1]

    • 注意初始化,f[0,0,0]客观存在所以赋值为1。最终答案为f[m,n,0]

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;
typedef long long LL;
const int N=210,MOD=1e9+7;

char s[N];
LL f[N][N][N];

void solve() {
    int n,m;
    scanf("%d%d%s",&n,&m,s+1);
    
    for(int i=0;i<=m;i++) //初始化
        for(int j=0;j<=n;j++)
            for(int k=0;k<=m;k++) 
                f[i][j][k]=0;
    f[0][0][0]=1;
    
    for(int i=1;i<=m;i++)
        for(int j=0;j<=min(i,n);j++)//优化
            for(int k=0;k<=i;k++) {
              //这样写编译就会爆数组越界
                //if(k) f[i][j][k]+=f[i-1][j-(s[j]=='(')][k-1] %= MOD;
                //f[i][j][k]+=f[i-1][j-(s[i]==')')][k+1] %= MOD;
                
              if(k) {//b[i]填'(',注意k需要>0,否则数组越界
                    if(s[j]=='(')f[i][j][k]+=f[i-1][j-1][k-1];
                    else f[i][j][k]+=f[i-1][j][k-1];
                    f[i][j][k]%=MOD;
                }
              //b[i]填')'
                if(s[j]==')') f[i][j][k]+=f[i-1][j-1][k+1];
                else f[i][j][k]+=f[i-1][j][k+1];
                f[i][j][k]%=MOD;
            }
    printf("%lld\n",f[m][n][0]);
}

int main() {
    int T;
    scanf("%d",&T);
    while(T--) solve();
    
    return 0;
}

L.Link with Level Editor I

  • 题意

    有n个世界,每个世界有若干个节点以及若干条有向边;游戏开始从点1出发,游戏中处于某点时,可以选择在这个世界内走向有边的点,也可以选择待着不动直接传送至下一个世界的同一点。问从点1走到m点所经过的最小世界数为多少?

  • 题解

    对于第i个世界可以从第i-1个世界走过来,答案也可以从上一世界的值转移过来,因此可以考虑dp

    状态定义:

    f[i,j]表示可以从1点走到第i个世界的j点的方案中,点1最晚出发的世界编号。由于内存不够,但每次转移只跟上一世界相关,所以滚动数组优化。now[x]表示从1点走到当前世界x点的方案中,点1最晚出发的世界编号。pre数组则表示上一世界的信息。

    状态转移

    对于第i个世界的某条边u->v,可以从上一世界的u点转移过来
    n o w [ v ] = m a z ( n o w [ v ] , p r e [ u ] ) now[v]=maz(now[v],pre[u]) now[v]=maz(now[v],pre[u])
    **注意:**当u为起点时,直接从当前世界走最优,如果不特殊处理使得答案更新错误

  • 代码

#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int main()
{
	ios::sync_with_stdio(0), cin.tie(0);
	int n, m, ans = INF;//ans求最小值,直接初始化为无穷大
	cin >> n >> m;
	vector<int> pre(m + 1), now(m + 1);
	for (int i = 1; i <= n; i++)
	{
		int u, v, k; cin >> k;
		now[1] = i;//在能从点1到达i世界中的1点的最晚世界编号为i
		while (k--)
		{
			cin >> u >> v;//u->v有向边
			now[v] = max(now[v], pre[u]);//向前面的世界转移
			if (u == 1) now[v] = i;//如果边的起点为1,那么从1到世界i的点v的最晚世界编号就是i本身
		}
    if(now[m]) ans = min(ans, i-now[m]+1);//如果某个世界的到达点m的值被更新了,意味着能到点m,所以更新答案
		pre = now;//滚动一下数组
	}
	if (ans == INF) ans = -1;//如果答案未被更新,意味着没有方案
	cout << ans;
	return 0;
}

D.ink with Game Glitch

  • 题意

    • n个物品,m个转换形式b->d(c/a),a个b可以转换成c个d
    • 所给的转换形式中存在无限循环转换,(1个x变2个y,1个y变2个x)
    • 现在给每个转换形式的比率都加上一个系数w(0<w<1)
    • 求使得不存在无限转换可能的最大w值
  • 题解

    • 建图,物品看成点,转换比率看成有向有权边
    • 当存在一个环,且环的所有比率乘积大于1,那么存在无限循环转换
    • 由于边多,所以乘法计算多次(若c/a < 1)会导致答案为0。所以不等式两边取log,即上一点转换为:环的所有边log(比率)之和大于0存在无限转换。等价于:环的所有边-log(比率)之和小于0存在无限循环。
    • 那么题目转换为找到一个最大的w是的图不存在负环,负环的寻找可以使用spfa,w的取值可以用二分,因为w存在单调性,当w越大越容易无限转换
  • 代码

#include <iostream>
#include <cstring>
#include <cmath>
#include <queue>

using namespace std;
const int N=1010,M=2010;
const double eps=1e-8;

int n,m;
int h[N],e[M],ne[M],idx;
double w[M];

void add(int a,int b,double c) {//a加上一条指向b的边,边权为c
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int cnt[N];//记录每点到起点的经过边数
bool st[N];//某点是否在队列当中
double dis[N];//记录某点到起点的距离

bool spfa(double mid){//判断有无负环
    queue<int> q;
    for(int i=1;i<=n;i++){
        dis[i]=0;cnt[i]=0;//因为每次二分都进行一次spfa,一定初始化!
        q.push(i);
        st[i]=true;
    }

    while(!q.empty()){
        int t=q.front();
        q.pop();
        st[t]=false;

        for(int i=h[t];i!=-1;i=ne[i]){
            int j=e[i];
            if(dis[j]>dis[t]+w[i]+mid){
                dis[j]=dis[t]+w[i]+mid;

                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n)return true

                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }

    return false;//所有可疑点都没有负环,所以没有负环
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin>>n>>m;
    memset(h,-1,sizeof h);//初始化链表头
    while(m--) {
        int a,b,c,d;
        cin>>a>>b>>c>>d;
        add(b,d,-log(c*1.0/a));//加边以及负转换比率
    }
    
    double l=0,r=1;//二分系数w
    while(r-l>eps) {
        double mid=(l+r)/2;
        if(spfa(-log(mid))) r=mid;//如果有负环意味着无限转换,意味着w太大,所以w应该在更小的范围找
        else l=mid;
    }
    printf("%lf\n",r);
    
    return 0;
}

C.Link with Nim Game

  • 题意

    • 普通版的Nim游戏,但附加了条件:必胜者希望快速结束,必败者希望拖延

    • 输出游戏进行的总轮数,以及先手第一轮可选择的方案数

  • 题解

    • Nim游戏可得结论:异或总和为0时先手必败,否则必胜。

    • 可证:输的人总能只取一个石子,并且使得对手下一轮也只能拿一个来拖延

    • 先手必胜

      先手第一轮应该取走尽可能多的石子maxv,由结论剩下的轮数就是一人拿一个,所以游戏轮数为石子总和tot-max+1,而可选方案就是所有可以取走maxv的堆数。

      如何求解maxv?对于每一堆都去取最大量石子使得异或和为0(使对方转为必败态),取所有堆的最大值即可。

      其中一堆的最大值如何求?假设我们取走之后剩余x个,最大值就是a[i]-x那么相当于我们先拿走了a[i]个(suma[i]),再放进去x个(suma[i]x),要使得`sum^a[i]^x=0`,两边同异或suma[i]可以解得x=sum^a[i],所以某一堆最大值为a[i]-sum^a[i]

    • 先手必败

      先手第一轮就应该只拿一个,所以总轮数为石子总和tot,可选方案是所有堆中,先手拿走一个之后后手也只能拿一个,符合种情况的堆数。

      如何统计堆数?首先把所有堆都拿走一个,并且记录拿走一个之后的总异或和,对于这种情况的异或和放入set并去重(即只保存拿走一个之后能到达的所有状态);再检验这些状态是否能让后手在下一轮只拿一个,不能就去掉这个不合法状态,那么set中剩下的就是先手拿走一个后使得后手只能拿一个的状态;最后检验所有堆在拿走一个后是否能到达set中的合法状态,记录合法的堆数,堆数即为可选方案。

  • 代码

#include <iostream>
#include <algorithm>
#include <set>

using namespace std;
const int N=1e5+10;
#define int long long//爆int了,哭哭

int n,a[N];

void solve() {
    cin>>n;
    
    int sum=0,tot=0;//异或和,数量和
    for(int i=0;i<n;i++) cin>>a[i],sum^=a[i],tot+=a[i];
    
    if(sum) {//先手必胜
        int maxv=0,ans=0;//第一轮最大取走数量,先手第一轮方案数
        for(int i=0;i<n;i++) {//找最大取走数
            int t=a[i]-(sum^a[i]);
            maxv=max(maxv,t);
        }
        for(int i=0;i<n;i++) {//记录方案数
            int t=a[i]-(sum^a[i]);
            if(maxv==t) ans++;
        }
        cout<<tot-maxv+1<<" "<<ans<<'\n';
    }
    else {//先手必败
        set<int> S,T;
        for(int i=0;i<n;i++) {//先手取走一个后的所有异或状态
            int t=sum^a[i]^(a[i]-1);//取走一个
            S.insert(t);//记录且去重
        }
        for(auto SUM:S) {//检查每个(先手取走一个后的异或状态)是否能让后手也只拿一个
            for(int i=0;i<n;i++) {//遍历后手选择情况
                int t=a[i]-SUM^a[i];//后手最多可取走a[i]堆中的个
                if(t>1) T.insert(SUM);//如果可取走不止一个,说明这个状态不最优,存入待删除的set
            }
        }
        for(auto it:T) S.erase(it);//删除不合法状态
        
      
      
      //以上预处理出了所有先手取走一个且后手也只能拿一个的状态
        int ans=0;//记录方案数
        for(int i=0;i<n;i++)//遍历所有先手第一轮拿的堆
            if(S.count(sum^a[i]^(a[i]-1)))//如果该堆取一个的状态是合法的,那么方案数+1
              ans++;
        cout<<tot<<" "<<ans<<'\n';
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int t;
    cin>>t;
    while(t--) solve();
    
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值