• 暑期集训模拟赛2


    前言

    今天算是有一些进步吧,(T4)(zsq)学长友情讲解

    NO.1 Layout

    Descrption

    和人类一样,奶牛们在打饭的时候喜欢和朋友站得很近。
    约翰的编号为 (1)(n)(n(2le nle 1000)) 只奶牛正打算排队打饭。现在请你来安排她们,让她们在数轴上排好队。奶牛的弹性很好,同一个坐标可以站无限只奶牛,排队的顺序必须和她们编号的顺序一致。有 (M) 对奶牛互相爱慕,她们之间的距离不能超过一定的值,有 (K) 对奶牛互相敌视,她们的距离不能小于一定的值。
    那么,首尾奶牛的最大距离是多少呢?

    Input

    第一行输入 (n,M,K,(0<M,Kle 5000))
    接下来 (M) 行每行三个整数(x,y,z),表示编号为 (x)(y) 的两头奶牛之间的距离最大不超过 (z)
    再接下来 (K) 行每行三个整数 (a,b,c),表示编号为 (a)(b) 的两头奶牛之间的距离最少为(c)

    Output

    如果没有合理方案,输出 (−1),如果首尾两头牛的距离可以无限大,输出 (−2),否则输出一个整数表示首尾奶牛的最大距离。

    Sample Input

    4 2 1
    1 3 10
    2 4 20
    2 3 3

    Sample Output

    27

    Hint

    四只牛分别在 (0,7,10,27)

    分析

    其实就是一道裸的差分约束题,我们只需要从(0)开始建一个超级源点,并且反着每两个相邻点直间建边,边权为0,然后先从(0)开始(spfa),判断是否有负环和无限大,如果没有再从一开始,输出到(n)的最短距离。

    代码

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn = 1e3+10;
    const int maxm = 5e3+10;
    struct Node{
    	int v,next,val;
    }e[maxm<<2];
    int n,m,k;
    int head[maxn],vis[maxn],dis[maxn];
    int tot,c[maxn];
    void Add(int x,int y,int z){
    	e[++tot].v = y;
    	e[tot].next = head[x];
    	e[tot].val = z;
    	head[x] = tot;
    }
    queue<int>q;
    int spfa(int s){
    	memset(dis,0x3f,sizeof(dis));
    	memset(vis,0,sizeof(vis));
    	memset(c,0,sizeof(vis));
    	dis[s] = 0;
    	vis[s] = 1;
    	q.push(s);
    	while(!q.empty()){
    		int x = q.front();
    		q.pop();
    		vis[x] = 0;
    		c[x]++;
    		if(c[x]>n)return -1;//判负环
    		for(int i=head[x];~i;i=e[i].next){
    			int v = e[i].v;
    			int w = e[i].val;
    			if(dis[v] > dis[x] + w){
    				dis[v] = dis[x] + w;
    				if(!vis[v]){
    					vis[v] = 1;
    					q.push(v);
    				}
    			}
    		}
    	}
    	if(dis[n]==0x3f3f3f3f)return -2;
    	return dis[n];
    }
    int main(){
    	memset(head,-1,sizeof(head));
    	scanf("%d%d%d",&n,&m,&k);
    	for(int i=1;i<=m;++i){//不等式建边
    		int x,y,z;
    		scanf("%d%d%d",&x,&y,&z);
    		Add(x,y,z);
    	}
    	for(int i=1;i<=k;++i){
    		int x,y,z;
    		scanf("%d%d%d",&x,&y,&z);
    		Add(y,x,-z);
    	}
    	for(int i=1;i<n;++i){//反着建一个边
    		Add(i+1,i,0);
    	}
    	for(int i=1;i<=n;++i){//超级源点
    		Add(0,i,0);
    	}
    	int jl = spfa(0);
    	if(jl<=-1){//判断负环和无穷大
    		printf("%d
    ",jl);
    		return 0;
    	}
    	else printf("%d
    ",spfa(1));
    	return 0;
    }
    

    NO.2 游戏

    Descrption

    (Mirko)(Slavko) 爱玩弹球戏。在一个令人激动的星期五,(Mirko)(Slavko) 玩了一把弹球游戏。(Mirko) 构建一个有向图,所有顶点最多有 (1) 条出边。弹球从 (1) 个顶点出发可以沿着一条边移动到它的邻接点,只要它存在,而且它会继续移动到后者的邻接点去,直到最后到达一个找不到出边的顶点才停下来。如果不存在这样的点,弹球可能无限运动下去。
    为了确信 (Slavko)理解游戏的规则,(Mirko) 将发起一系列询问,询问的类型如下:
    (1 X) :除非弹球陷入循环,弹球从 (X) 出发,最终将在哪个点停下来。
    (2 X):删除 (X) 的出边(保证该边总是存在)
    注意:询问是按顺序执行的。

    Input

    第一行包含一个正整数 (N(1le Nle 300000)),表示图的定点数。
    第二行包含由空格隔开 (N) 个正整数,第 (i) 个数表示从 (i) 顶点可以通过出边到达的定点编号。(0) 表示该点没有出边。
    接下来的一行包含 (1) 个整数 (Q(1le Qle 300000)),表示询问的次数。格式如上所示。

    Output

    对于第 (1) 类询问,输出弹球停止时所在顶点编号,每行 (1) 个,按照查询的顺序输出。如果弹球无法停止,则输出 (CIKLUS).

    Sample Input

    3
    2 3 1
    7
    1 1
    1 2
    2 1
    1 2
    1 1
    2 2
    1 2

    Sample Output

    CIKLUS
    CIKLUS
    1
    1
    2

    分析

    因为涉及到好多删边,查询能到哪里的操作,所以我们用并查集来实现,把去点作为当前点的父亲。首先记录每一个操作,然后首先把边删掉,也就是先进行(2)操作。然后记录一下原来的父子关系,方便还原。然后我们开始倒序查询(1)操作,如果涉及到(2)操作,那么就还原,记录下来当前操作下的点的(fa),也就是答案,最后再进行正向的枚举,如果父亲不是(0),那么就输出父亲,否则输出无法停下。

    代码

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn = 3e5+10,maxm = 2e5+10;
    int fa[maxn];
    int ffa[maxn];//复制数组,便于还原
    int n;
    int a[maxn][2];
    int Find(int x,int jl){
    	if(jl>n)return fa[x] = 0;//如果为环,就让父亲为0。
    	if(x == fa[x])return x;
    	return fa[x] = Find(fa[x],jl+1);//每次记录经过的点加一。
    }
    int main(){
    	scanf("%d",&n);
    	for(int i=1;i<=n;++i){//初始化
    		fa[i] = i;
    	}
    	for(int i=1;i<=n;++i){
    		int x;
    		scanf("%d",&x);
    		if(x!=0)fa[i] = ffa[i] = x;//有连接的点就让复制数组和父亲数组就记录。
    	}
    	int Q;
    	scanf("%d",&Q);
    	for(int i=1;i<=Q;++i){//进行删除操作,也就是父亲为自己
    		scanf("%d%d",&a[i][0],&a[i][1]);
    		if(a[i][0] == 2){
    			fa[a[i][1]] = a[i][1];
    		}
    	}
    	for(int i=Q;i>0;--i){//反着枚举
    		if(a[i][0] == 1){
    			a[i][1] = Find(a[i][1],0);
    		}
    		else{
    			fa[a[i][1]] = ffa[a[i][1]];//还原
    		}
    	}
    	for(int i=1;i<=Q;++i){
    		if(a[i][0] == 1){
    			if(a[i][1]){//不为0就输出
    				printf("%d
    ",a[i][1]);
    			}
    			else{
    				printf("CIKLUS
    ");
    			}
    		}
    	}
    	return 0;
    }
    

    NO.3 数字(数位dp)

    Descrption

    一个数字被称为好数字当他满足下列条件:
    它有 (2 imes n) 个数位,(n) 是正整数(允许有前导 (0) )。
    构成它的每个数字都在给定的数字集合 (S) 中。
    它前 (n) 位之和与后 (n) 位之和相等或者它奇数位之和与偶数位之和相等。
    例如对于 (n=2,S={1,2}),合法的好数字有 (1111,1122,1212,1221,2112,2121,2211,2222) 这样 (8) 种。
    已知 (n),求合法的好数字的个数 (mod 999983)

    Input

    第一行一个数 (n)
    接下来一个长度不超过 (10) 的字符串,表示给定的数字集合(不存在重复的数字)。

    Output

    一行,一个整数表示合法的好数字的个数 (mod 999983)

    Sample Input

    2
    0987654321

    Sample Output

    1240

    Hint

    对于 (20\%) 的数据,(nle 7)
    对于 (100\%) 的数据,(nle 1000,|S|le 10)

    分析

    (20)分的直接暴力就行,后边的需要用到(dp)
    我们定义(f[i][j])表示前(i)个数字和为(j)的情况数,那么我们很容易推出来(f[i][j])的转移就是:

    [f[i][j] = sum_{j=0}^{max(a_i)}sum_{k=1}^{len(s)}f[i-1][j-a[k]] ]

    这样我们就处理出来了所有的(f[i][j]),然后开始统计答案。
    首先考虑前(n)和后(n)个数和相等的情况,那么我们只需要考虑前(n)的情况就行了,总的情况数就是前(n)(f[n][i])的平方。
    然后考虑奇数和偶数位和相等,其实也是一样的,我们只需要(n)个数,两个长度位(n)的序列交叉在一起就行,那么答案与上一种一样,所以最终的答案就是(2)倍。也就是:

    [ans+=sum_{i=0}^{n imes max(a_n)}(f[n][i])^2 ]

    而因为这两种情况里有交叉也就是多算了的情况,所以需要减去。
    假设左边的奇数序列是(a),那么多算的情况就是右边的偶数序列是(a),那么减去这种就行了。前(n)位中奇数有(frac{n+1}{2})个,偶数位有(frac{n}{2})个,那么就可以得到需要减去的东西,即:

    [ans-=sum_{i=0}^{frac{n+1}{2} imes max(a_n)}(f[frac{n+1}{2}][i])^2 imes sum_{j=0}^{frac{n}{2} imes max(a_n)}(f[frac{n}{2}][j])^2 ]

    代码

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int maxn = 1e3+10;
    const int maxm = 19;
    const int mod = 999983;
    char s[maxm];
    int a[maxm];
    int n;
    ll f[maxn][maxn*9];
    int main(){
        scanf("%d%s",&n,s+1);
        a[0] = strlen(s+1);
        for(int i=1;i<=a[0];++i){
            a[i] = s[i]-'0';//处理出每一位
        }
        f[0][0] = 1;
        for(int i=1;i<=n;++i){
            for(int j=0;j<=i*9;++j){
                for(int k=1;k<=a[0];++k){
                    if(j>=a[k]){//计算总的情况
                        f[i][j] = (f[i][j] + f[i-1][j-a[k]])%mod;
                    }
                }
            }
        }
        ll ans = 0;
        for(int i=0;i<=n*9;++i){
            ans = (ans + 2*f[n][i]*f[n][i])%mod;//算出总情况
        }
        int len1 = (n+1)/2;
        int len2 = n/2;
        ll ans1 = 0;
        ll ans2 = 0;
        for(int i=0;i<=len1*9;++i){//算出奇数位
            ans1 = (ans1 + f[len1][i]*f[len1][i]%mod)%mod;
        }
        for(int i=0;i<=len2*9;++i){//算出偶数位也就是上边式子的后半部分
            ans2 = (ans2 + f[len2][i]*f[len2][i]%mod)%mod;
        }
        ans = (ans - ans1*ans2%mod+mod)%mod;//减去
        printf("%lld
     ",ans%mod);
        return 0;
    }
    

    NO.4 水站

    题目描述

    已知有一个(n)层的水站:
    (W_i)表示未操作之前第 (i) 层的已有水量;
    (L_i)表示第 (i) 个水站能够维持或者储存的水的重量;
    (P_i)表示在第 (i) 层进行减压放水操作所需的费用.
    被压减放水层所储存的所有水都将流向下一层。如果第(i)层的水量比(L_i) 大,则这一层也会(自动)减压(不需要任何费用)。
    现在想要使最后一层减压(第 (n)级),求最少的花费。这个任务现在交给了你。

    输入格式

    每个输入的第一行包含一个自然数(n(1le nle 150000))
    接下来 (n) 行每行包含 (3) 个数 (W_i,L_i,P_i(0le W_i,L_i,P_ile 15000))

    输出格式

    第一行输出所需的最小费用
    第二行若干个整数,从小到大输出必须减压的层的编号。

    样例

    样例输入

    3
    1000 1000 1
    0 1000 2
    2 10 100

    样例输出

    3
    1 2

    数据范围与提示

    给第一层和第二层减压
    (30\%:nle 5000)
    (100\%:nle 150000)

    分析

    看到题目我们可以分析一下, 如果上边的一堆都减压了,但是有一个地方没有减压,这是对最后一层没有用的,所以减压的话肯定是一串连到最后一层,很容易能想出(O(n^2))的暴力。但是这样对于这个数据应该是过不去的(优良传统写正解),所以我们用到差分和前缀和。

    我们设(c_i)为从(i)(n)一直放水的费用。我们考虑其中的每一个点能否不花钱自动防水,只有从(i)(k)的所有水都不能让他自动放水,那么其中应该要包含(k)的费用,而这个(i)肯定也是连续的一段,所以我们需要找到最小的那个(i),然后在这一段内的(c_i)都加上(p_k),这里用差分数组就可以。时间复杂度(O(n log n))

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn = 15e4+10;
    int w[maxn],l[maxn],p[maxn];
    int n;
    int jl;
    int c[maxn],sum[maxn];
    int main(){
        scanf("%d",&n);
        for(int i=1;i<=n;++i){
            scanf("%d%d%d",&w[i],&l[i],&p[i]);
            sum[i] = sum[i-1]+w[i];//记录前缀和,二分用
        }
        for(int i=1;i<=n;++i){
            int x = lower_bound(sum,sum+i+1,sum[i]-l[i])-sum;//二分找到上边所说的最小的i(这里是x)
            x++;
            c[x] += p[i];//差分数组,这个点的值加p[i]
            c[i+1] -= p[i];//后边这个点的值减去p[i]
        }
        int ans = 0x3f3f3f3f;
        for(int i=1;i<=n;++i){
            c[i] = c[i] + c[i-1];//差分数组记录的,所以想让这一段都加上,就需要每一次都加上一个的
            if(ans > c[i]){//记录最小
                ans = c[i];
                jl = i;
            }
        }
        cout<<ans<<endl;//输出最小花费
        int tot = 0;
        for(int i=jl;i<=n;++i){//找到哪几个需要花钱
            tot += w[i];
            if(tot<=l[i])printf("%d ",i);//输出
        }
        return 0;
    }
    
  • 相关阅读:
    方法的形式参数是类名的时候如何调用
    成员变量与局部变量的区别
    如何选择适合自己的Linux版本
    MySQL数据库的特点和优势
    面向对象学生类和手机类的使用
    面向对象基本概述
    数据库概念介绍
    二位数组的求和
    二维数组遍历
    SpringBoot与SpringMVC的区别是什么?
  • 原文地址:https://www.cnblogs.com/Vocanda/p/13325142.html
Copyright © 2020-2023  润新知