2022牛客蔚来杯第二场
文章目录
- 2022牛客蔚来杯第二场
- [G.Link with Monotonic Subsequence](https://ac.nowcoder.com/acm/contest/33187/G)
- [J.Link with Arithmetic Progression](https://ac.nowcoder.com/acm/contest/33187/J)
- [K.Link with Bracket Sequence I](https://ac.nowcoder.com/acm/contest/33187/K)
- [L.Link with Level Editor I](https://ac.nowcoder.com/acm/contest/33187/L)
- [ D.ink with Game Glitch](https://ac.nowcoder.com/acm/contest/33187/D)
- [C.Link with Nim Game](https://ac.nowcoder.com/acm/contest/33187/C)
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,否则数组越界)
-
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[i−1,j−1,k−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[i−1,j,k−1]
b[i]放’)’
-
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[i−1,j,k+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[i−1,j−1,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;
}