2022牛客暑期多校第二场

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

【题意】

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

【题解】

最小值为$\left \lceil \sqrt{n} \right \rceil$。

可行性:构造方法为依次排列$\left \lceil \sqrt{n} \right \rceil$个上升/下降序列,如 2 1 / 5 4 3 / 8 7 6

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

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

  • $x \leq x\ (x \in P)$
  • $x \leq y$且$y \leq x$ $\Rightarrow$ $x=y$
  • $x \leq y$且$y \leq z$ $\Rightarrow$ $x \leq z$

链:$P$的子集$Q={x_1,x_2, \cdots, x_k}$使得$x_1 \leq x_2 \leq \cdots \leq x_k$,且$x_i \neq x_j\ (i \neq j)$

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

最长反链:$P$的最大子集$S$,使得$\forall i \neq j,\ x_i \nleqslant x_j$

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

  • $P={(1,p_1),(2,p_2),\cdots,(n,p_n)}$
  • $\leq$关系定义为$(i,p_i) \leq (j,p_j) \Leftrightarrow i \leq j\ and\ p_i \leq p_j$
  • 一条链即为一个上升子序列
  • 最长反链即为最长下降子序列

因此,根据 Dilworth 定理,最长下降子序列 = 最小(上升子序列)划分,即 lds = #is。由于 #$is \cdot lis \geq n$,那么$lds \cdot lis \geq n$,那么$max(lis,lds) \geq \left \lceil \sqrt{n} \right \rceil$。

【代码】

#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 的合法括号串的数量。$1 \leq T \leq 100, 1 \leq n,m \leq 200$

【题解】

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

$$ dp[i][j][k](j > 0)\ +=\ \left\{\begin{matrix}
left\ bracket \left\{\begin{matrix}
dp[i-1][j-1][k-1]\ \ if\ s[j]='(' \\
dp[i-1][j][k-1]\ \ if\ s[j{\color{Red} +1}]=')'\end{matrix}\right.\\
right\ bracket\ (k \geq i-k) \left\{\begin{matrix}
dp[i-1][j-1][k]\ \ if\ s[j]=')'\\
dp[i-1][j][k]\ \ if\ s[j{\color{Red} +1}]='('\end{matrix}\right.\end{matrix}\right. $$

$$ dp[i][0][k] = (left\ bracket\ only)\ dp[i-1][0][k-1]\ \ 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 的环。$2 \leq n \leq 1000, 2 \leq m \leq 2000$

【题解】

首先容易发现 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。$1 \leq n \leq 10^4, 2 \leq m \leq 2 \times 10^3$

【题解】

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

$$ dp[i][j] = \left\{\begin{matrix}
i,\ \ \ if\ j=1\ or\ the\ nodes\ 1\ points\ to \\
max(dp[i-1][j],dp[i{\color{Red} -1}][j']),\ \ \ otherwise\end{matrix}\right. $$

$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' = X_{norm}X_{norm}^TY $$

其中$X_{norm}$为$X$沿行归一化后所得矩阵,维度为$n \times k$。$Y$的维度为$n \times d$。$3 \leq n \leq 10^4, 1 \leq k,d \leq 50$,时限3s。

如果按照顺序计算,$X_{norm}X_{norm}^T$的复杂度为$O(n^2k)$,$(X_{norm}X_{norm}^T)Y$的复杂度为$O(n^2d)$,不可行。解决方法根据结合律改变运算顺序,先计算$X_{norm}^TY$,复杂度为$O(knd)$,再计算$X_{norm}(X_{norm}^TY)$,复杂度为$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 层所需最少时间。$1 \leq n,m \leq 2 \times 10^5, 1 \leq k \leq 10^9$

【题解】

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

【实现】

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

【代码】

#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 @ 2022-07-28 15:21  Maaaaax  阅读(34)  评论(0)    收藏  举报