暑期集训模拟赛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]\),然后开始统计答案。
首先考虑前\(n\)和后\(n\)个数和相等的情况,那么我们只需要考虑前\(n\)的情况就行了,总的情况数就是前\(n\)的\(f[n][i]\)的平方。
然后考虑奇数和偶数位和相等,其实也是一样的,我们只需要\(n\)个数,两个长度位\(n\)的序列交叉在一起就行,那么答案与上一种一样,所以最终的答案就是\(2\)倍。也就是:
而因为这两种情况里有交叉也就是多算了的情况,所以需要减去。
假设左边的奇数序列是\(a\),那么多算的情况就是右边的偶数序列是\(a\),那么减去这种就行了。前\(n\)位中奇数有\(\frac{n+1}{2}\)个,偶数位有\(\frac{n}{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;
}