暑期集训模拟赛2

前言

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

NO.1 Layout

Descrption

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

Input

第一行输入 \(n,M,K,(0<M,K\le 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\n",jl);
		return 0;
	}
	else printf("%d\n",spfa(1));
	return 0;
}

NO.2 游戏

Descrption

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

Input

第一行包含一个正整数 \(N(1\le N\le 300000)\),表示图的定点数。
第二行包含由空格隔开 \(N\) 个正整数,第 \(i\) 个数表示从 \(i\) 顶点可以通过出边到达的定点编号。\(0\) 表示该点没有出边。
接下来的一行包含 \(1\) 个整数 \(Q(1\le Q\le 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\n",a[i][1]);
			}
			else{
				printf("CIKLUS\n");
			}
		}
	}
	return 0;
}

NO.3 数字(数位dp)

Descrption

一个数字被称为好数字当他满足下列条件:
它有 \(2\times 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\%\) 的数据,\(n\le 7\)
对于 \(100\%\) 的数据,\(n\le 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\times 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}\times max(a_n)}(f[\frac{n+1}{2}][i])^2\times \sum_{j=0}^{\frac{n}{2}\times 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\n ",ans%mod);
    return 0;
}

NO.4 水站

题目描述

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

输入格式

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

输出格式

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

样例

样例输入

3
1000 1000 1
0 1000 2
2 10 100

样例输出

3
1 2

数据范围与提示

给第一层和第二层减压
\(30\%:n\le 5000\)
\(100\%:n\le 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;
}
posted @ 2020-07-16 21:25  Vocanda  阅读(142)  评论(1编辑  收藏  举报