2022牛客暑期多校第二场

【构造+打表找规律/Dilworth定理】G

【题意】

构造一个 n 的排列,使得 max(lis,lds) 最小。(lis为最长上升子序列长度,lds为最长下降子序列长度)

【题解】

最小值为n

可行性:构造方法为依次排列n个上升/下降序列,如 2 1 / 5 4 3 / 8 7 6

正确性:Dilworth定理 - 偏序集的最长反链 = 最小链分划

偏序集:能够定义满足以下条件的关系的集合P

  • xx (xP)
  • xyyx x=y
  • xyyz xz

链:P的子集Q=x1,x2,,xk使得x1x2xk,且xixj (ij)

最小链划分:将P划分成m条互不相交的链,使得m最小

最长反链:P的最大子集S,使得ij, xixj

此问题中,pi表示排列中的第i个数,则:

  • P=(1,p1),(2,p2),,(n,pn)
  • 关系定义为(i,pi)(j,pj)ij and pipj
  • 一条链即为一个上升子序列
  • 最长反链即为最长下降子序列

因此,根据 Dilworth 定理,最长下降子序列 = 最小(上升子序列)划分,即 lds = #is。由于 #islisn,那么ldslisn,那么max(lis,lds)n

【代码】

复制代码
#include <bits/stdc++.h>
using namespace std;
const double eps=1e-9;
int T,n,num,cnt,r,idx;
int main()
{
  scanf("%d",&T);
  while (T--){
    scanf("%d",&n);
    num=(int)(sqrt((double)n)-eps)+1;   //节数
    cnt=n/num;   //每节有几个数
    r=n%num;   //余数放到最后r节
    idx=0;
    for (int i=1;i<=num-r;i++){
      idx+=cnt;
      for (int j=idx;j>idx-cnt;j--) printf("%d ",j);
    }
    for (int i=num-r+1;i<=num;i++){
      idx+=cnt+1;
      for (int j=idx;j>idx-cnt-1;j--) printf("%d ",j);
    }
    printf("\n");
  }
  return 0;
}
复制代码

【括号串dp】K

【题意】

给定一个长度为 n 的括号串s(不一定合法),求以 s 为子序列的长度为 m 的合法括号串的数量。1T100,1n,m200

【题解】

dp[i][j][k]:长度为 i,与 s 的最长公共子序列长为 j(不要包含至第 j 位,也不要以第 j 位结尾,否则都会重复计算!), 有 k 个左括号的括号串(不一定合法)的个数

dp[i][j][k](j>0) += {left bracket{dp[i1][j1][k1]  if s[j]=(dp[i1][j][k1]  if s[j+1]=)right bracket (kik){dp[i1][j1][k]  if s[j]=)dp[i1][j][k]  if s[j+1]=(

dp[i][0][k]=(left bracket only) dp[i1][0][k1]  if s[0]=)

四行为平行关系。注意标红的 +1,很重要!!!因为只有当 s[j+1] = ')' 时,填左括号才能保证由 dp[i-1][j][k-1] 转移过来后最长公共子序列长度仍为 j。

初始条件为 dp[1][0/1][0/1],根据 s[0] 赋值。所求答案为 dp[m][n][m/2]。

【代码】

复制代码
#include <bits/stdc++.h>
using namespace std;
const int N=205;
const int mod=1e9+7;
int T,n,m,dp[N][N][N>>1];
char s[N];
int main()
{
  scanf("%d",&T);
  while (T--){
    scanf("%d%d",&n,&m);
    getchar();
    scanf("%s",s+1);
    dp[1][0][0]=0; dp[1][1][0]=0;
    if (s[1]=='(') dp[1][0][1]=0,dp[1][1][1]=1;
    else dp[1][0][1]=1,dp[1][1][1]=0;
    for (int i=2;i<=m;i++)
      for (int j=0;j<=min(i,n);j++)
        for (int k=1;k<=min(i,m/2);k++){
          dp[i][j][k]=0;
          if (j==0){
            if (s[1]==')') dp[i][j][k]=dp[i-1][j][k-1]; //只可能填左括号
            continue;
          }
          //填左括号
          if (s[j]=='(') dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k-1])%mod;
          if (s[j+1]==')'||j==n) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k-1])%mod;  //j==n特判
          //填右括号
          if (k>=i-k){
            if (s[j]==')') dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k])%mod;
            if (s[j+1]=='('||j==n) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k])%mod;
          }
        }
    printf("%d\n",dp[m][n][m/2]);
  }
  return 0;
}
复制代码

【二分+判负环+取log】D

【题意】

给定一张 n 点 m 边的有向图,边权为实数,图中存在边权乘积大于 1 的环。求最大的 w,使得每条边权乘以 w 后,图中不存在边权乘积大于 1 的环。2n1000,2m2000

【题解】

首先容易发现 w 与边权乘积具有单调关系,因此想到二分答案。

check(w) 的功能是判断图中是否存在边权乘积大于 1 的环(在求最长路的过程中存在使路径越来越长的环),这相当于判负环(在求最短路的过程中存在使路径越来越短的环),可以用 SPFA 解决,时间复杂度 O(nm)。不过由于图不一定连通,单源最长路可能无法覆盖全图。只需对原 SPFA 稍作修改:dis[u] 表示任意点到 u 点的最长路,初始化为 1(自己到自己,根据题意应为 1),并在开始时将全部点加入队列;cnt[u] 表示最长路径经过的边的数量,如果 cnt[u] >= n,说明图中存在边权乘积大于 1 的环(其实这也是连通图判负环的方法之一)。

除此之外,边权乘积可能很大,对此可以进行取 log 操作,变乘为加。

【实现】

对于实数的二分,退出循环条件为 r-l>eps,其中 eps = 精度要求 / 10。

【代码】

复制代码
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m,b,d,cnt[N];
double a,c,l,r,mid,ans,dis[N];
bool book[N];
struct node{
  int v;
  double w;
};
vector<node> out[N];
bool check(double k){
  queue<int> q;
  memset(cnt,0,sizeof(cnt));
  for (int i=1;i<=n;i++){
    dis[i]=0.0;   //取log,log(1)=0
    q.push(i);    //所有点入队
    book[i]=1;
  }
  while (!q.empty()){
    int u=q.front();
    q.pop();
    book[u]=0;
    int len=out[u].size();
    for (int i=0;i<len;i++){
      int v=out[u][i].v;
      if (dis[u]+out[u][i].w+k>dis[v]){
        dis[v]=dis[u]+out[u][i].w+k;
        cnt[v]=cnt[u]+1;   //记录最长路径经过边数
        if (cnt[v]>=n) return 0;
        if (book[v]==0){
          q.push(v);
          book[v]=1;
        }
      }
    }
  }
  return 1;
}
int main()
{
  scanf("%d%d",&n,&m);
  for (int i=1;i<=m;i++){
    scanf("%lf%d%lf%d",&a,&b,&c,&d);
    out[b].push_back({d,log(c/a)});
  }
  l=0.0;r=1.0;ans=0.0;
  while (r-l>1e-7){    //精度要求1e-6
    mid=(l+r)/2.0;
    if (check(log(mid))) l=mid,ans=mid;
    else r=mid;
  }
  printf("%.7lf",ans);
  return 0;
}
复制代码

【图dp】L

【题意】

有 n 张每张 m 个点的有向图,从中选择连续的 w 张,使得从第一张的 1 号点出发,在每张图中要么留在原地,要么走过一条边,然后到达下一张图中同一编号的点,最终到达第 w 张的 m 号点。求最小的 w。1n104,2m2×103

【题解】

不难设计状态 dp[i][j] 表示到达第 i 张图的 j 号点,最晚从第几张图的 1 号点出发。

dp[i][j]={i,   if j=1 or the nodes 1 points tomax(dp[i1][j],dp[i1][j]),   otherwise

j 表示指向j的点。注意标红的”-1“,不可以从 dp[i][j'] 转移,因为 dp[i][j'] 中存储的可能是已经在第 i 张图中走过一条边的值。

初始状态即转移式第一行,所求答案为 min{ i - dp[i][m] +1 }。

由于直接存储会爆内存,又发现每次转移只需要 i-1 的状态,于是将第一维滚动掉。

【实现】

滚动数组实现 dp 转移式中的第一行:读入第 i 张图的边之前,设上一张图的 1 号点位 i:dp[!cur][1]=i。

【代码】

复制代码
#include <bits/stdc++.h>
using namespace std;
const int M=2e3+5;
int n,m,l,u,v,dp[2][M],cur,ans=2147483647;
int main()
{
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;i++){
    dp[!cur][1]=i;   //转移式中第一行
    for (int j=2;j<=m;j++) dp[cur][j]=dp[!cur][j];
    scanf("%d",&l);
    for (int j=1;j<=l;j++){
      scanf("%d%d",&u,&v);
      dp[cur][v]=max(dp[cur][v],dp[!cur][u]);
    }
    if (dp[cur][m]) ans=min(ans,i-dp[cur][m]+1);
    cur=!cur;
  }
  if (ans!=2147483647) printf("%d",ans);
  else printf("-1");
  return 0;
}
复制代码

【矩阵乘法】I

【题意】略。

【题解】

把式子写成矩阵乘法的形式,其实就是 vectorization。写出式子为:

Y=XnormXnormTY

其中XnormX沿行归一化后所得矩阵,维度为n×kY的维度为n×d3n104,1k,d50,时限3s。

如果按照顺序计算,XnormXnormT的复杂度为O(n2k)(XnormXnormT)Y的复杂度为O(n2d),不可行。解决方法根据结合律改变运算顺序,先计算XnormTY,复杂度为O(knd),再计算Xnorm(XnormTY),复杂度为O(nkd),就可行了。

【代码】

复制代码
#include <bits/stdc++.h>
using namespace std;
int n,k,d;
double X[10005][55],Y[10005][55],sum,XtY[55][55],ans[10005][55];
int main()
{
  scanf("%d%d%d",&n,&k,&d);
  for (int i=1;i<=n;i++){
    sum=0.0;
    for (int j=1;j<=k;j++){
      scanf("%lf",&X[i][j]);
      sum+=X[i][j]*X[i][j];
    }
    sum=sqrt(sum);
    for (int j=1;j<=k;j++)   //沿行归一化
      X[i][j]/=sum;
  }
  for (int i=1;i<=n;i++)
    for (int j=1;j<=d;j++)
      scanf("%lf",&Y[i][j]);
  for (int i=1;i<=k;i++)   //计算Xnorm^T x Y
    for (int j=1;j<=d;j++)
      for (int z=1;z<=n;z++)
        XtY[i][j]+=X[z][i]*Y[z][j];
  for (int i=1;i<=n;i++)   //计算Xnorm x XtY
    for (int j=1;j<=d;j++)
      for (int z=1;z<=k;z++)
        ans[i][j]+=X[i][z]*XtY[z][j];
  for (int i=1;i<=n;i++){
    for (int j=1;j<=d;j++)
      printf("%.8lf ",ans[i][j]);
    printf("\n");
  }
  return 0;
}
复制代码

【贪心+实现】H

【题意】

楼高 k 层,有 n 个人要上下楼,每个人有起始楼层和目标楼层,电梯限载 m 人,每次下行必须到达第 1 层才能换上行。电梯走一层消耗一单位时间,上下人时间忽略,求从第 1 层出发,送完所有人,并回到第 1 层所需最少时间。1n,m2×105,1k109

【题解】

由于电梯每次下行都必须下到底层,因此我们希望每次上行到达的最高楼层最小。那么每次上行,优先选目标楼层高的人,每次下行,优先选起始楼层高的人,这样可以最小化每次上行所达最高层。值得一提的是,是否允许中途下电梯对答案没有影响。

【实现】

很奇妙的实现方法,想了好久,直接看代码吧。

【代码】

复制代码
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,k,a,b,cnt1,cnt2,ans1[N],ans2[N],cnt;
long long ans;
struct node{
  int pos,t;
  bool operator < (const node &x) const{
    if (pos!=x.pos) return pos<x.pos;   //priority_queue 符号相反
    return t>x.t;
  }
};
priority_queue<node> up,down;
int main()
{
  scanf("%d%d%d",&n,&m,&k);
  for (int i=1;i<=n;i++){
    scanf("%d%d",&a,&b);
    if (a<b) up.push({a,-1}),up.push({b,1});  //从上往下扫(上下行都是),所以上端点+1,下端点-1
    else down.push({a,1}),down.push({b,-1});
  }
  //cnt为当前楼层有多少人【需要】坐电梯
  //cnt1,cnt2分别表示需要上/下行多少趟
  while (!up.empty()){   //上行
    node u=up.top();
    up.pop();
    cnt+=u.t;
    if (cnt>cnt1*m) ans1[++cnt1]=u.pos;   //电梯每走一趟会运 m 人(如果需 >= 供),
                         //如果当前需要坐电梯的人数 > 当前能运的人数,
                         //那么趟数+1,该趟到达最高层为当前层
} cnt=0; while (!down.empty()){ //下行 node u=down.top(); down.pop(); cnt+=u.t; if (cnt>cnt2*m) ans2[++cnt2]=u.pos; } for (int i=1;i<=max(cnt1,cnt2);i++) //统计答案 ans+=2ll*(max(ans1[i],ans2[i])-1ll); //每趟到达的最高层为上下行所达最高层取max printf("%lld",ans); return 0; }
复制代码

【容斥+多项式】E

TBC.


【Trie+线段树+倍增】F

TBC.

posted @   Maaaaax  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示