牛客暑假多校 2023 第四场

写在前面

比赛地址:https://ac.nowcoder.com/acm/contest/57358

  1. 那时,天下人的口音,言语,都是一样。
  2. 他们往东边迁移的时候,在示拿地遇见一片平原,就住在那里。
  3. 他们彼此商量说,来吧,我们要作砖,把砖烧透了。他们就拿砖当石头,又拿石漆当灰泥。
  4. 他们说,来吧,我们要建造一座城和一座塔,塔顶通天,为要传扬我们的名,免得我们分散在全地上。
  5. 耶和华降临,要看看世人所建造的城和塔。
  6. 耶和华说,看哪,他们成为一样的人民,都是一样的言语,如今既作起这事来,以后他们所要作的事就没有不成就的了。
  7. 我们下去,在那里变乱他们的口音,使他们的言语彼此不通。
  8. 于是,耶和华使他们从那里分散在全地上。他们就停工,不造那城了。
  9. 因为耶和华在那里变乱天下人的言语,使众人分散在全地上,所以那城名叫巴别(就是变乱的意思)。

—— 创世纪 - 十一

以下按个人向难度排序。

A

发现 \(t\) 可能会出现在 \(s\) 的内部,或者 \(t+s+t\) 的交界处。一个显然的想法是先构造内部没有 \(t\) 的一段,然后再考虑往两边添加使得交界处没有 \(t\)

发现构造全 0 或全 1 即可使内部没有 \(t\),在此基础上再考虑,发现直接令 \(s\) 为全 0 或全 1 即可。可以证明解至少为全 0 或全 1 中的一个。

构造 \(t+s+t\) 后 KMP 判断是否出现 \(t\) 即可。

//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
const int kN = 1e3 + 10;
//=============================================================
int n, m;
int fail[kN];
char t[kN], ans[3 * kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
void KMP() {
  fail[1] = 0;
  for (int i = 2, j = 0; i <= m; ++ i) {
    while (j && t[i] != t[j + 1]) j = fail[j];
    if (t[i] == t[j + 1]) ++ j;
    fail[i] = j;
  }  
}
bool Check0() {
  int n1 = 2 * m + n;
  for (int i = 1; i <= m; ++ i) ans[i] = t[i];
  for (int i = 1; i <= n; ++ i) ans[m + i] = '0';
  for (int i = 1; i <= m; ++ i) ans[n + m + i] = t[i];
  ans[n1 + 1] = 0;

  for (int i = 2, j = 0; i < n1; ++ i) {
    while (j && ans[i] != t[j + 1]) j = fail[j];
    if (ans[i] == t[j + 1]) ++ j;
    if (j == m) return 0;
  }
  for (int i = 1; i <= n; ++ i) printf("0");
  printf("\n");
  return 1;
}
bool Check1() {
  int n1 = 2 * m + n;
  for (int i = 1; i <= m; ++ i) ans[i] = t[i];
  for (int i = 1; i <= n; ++ i) ans[m + i] = '1';
  for (int i = 1; i <= m; ++ i) ans[n + m + i] = t[i];
  ans[n1 + 1] = 0;

  for (int i = 2, j = 0; i < n1; ++ i) {
    while (j && ans[i] != t[j + 1]) j = fail[j];
    if (ans[i] == t[j + 1]) ++ j;
    if (j == m) return 0;
  }
  for (int i = 1; i <= n; ++ i) printf("1");
  printf("\n");
  return 1;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    n = read();
    scanf("%s", t + 1); m = strlen(t + 1);
    KMP();
    if (Check0()) continue;
    if (Check1()) continue;
    printf("-1\n"); 
  }
  return 0;
}

L

典题,当年在 qbxt 做过。

对于同一行同一列,如果对它有多次修改,仅有最后一次修改是有效的,于是考虑倒着做,忽略重复操作的同时,维护后若干的操作已经将多少行/列置 0/1,遇到置 1 操作根据维护的信息累计贡献即可。

//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, m, q;
int opt1[kN], opt2[kN], opt3[kN];
int tag[2][kN], cnt[2][2];
char s[15];
LL ans;
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  n = read(), m = read(), q = read();
  cnt[0][0] = 0, cnt[0][1] = 0, cnt[1][0] = 0, cnt[1][1] = 0;
  for (int i = 1; i <= q; ++ i) {
    scanf("%s", s + 1);
    opt1[i] = s[1] == 'c';
    opt2[i] = read();
    scanf("%s", s + 1);
    opt3[i] = s[2] == 'n' ? 1 : -1; 
  }

  for (int i = q; i; -- i) {
    int type = opt1[i], p = opt2[i], key = opt3[i];
    if (tag[type][p]) continue;
    tag[type][p] = key;
    if (type == 0) {
      if (key == -1) {
        cnt[0][0] ++;
      } else {
        cnt[0][1] ++;
        ans += m - cnt[1][0] - cnt[1][1];
      }
    } else {
      if (key == -1) {
        cnt[1][0] ++;
      } else {
        ++ cnt[1][1];
        ans += n - cnt[0][0] - cnt[0][1];
      }
    }
  }
  printf("%lld\n", ans);
  return 0;
}

F

中国人的性情是总喜欢调和折中的,譬如你说,这屋子太暗,须在这里开一个窗,大家一定不允许的。但如果你主张拆掉屋顶,他们就来调和,愿意开窗了。

在某一轮操作中,设中位数为 \(a_m\),显然大于中位数的会选择删除极小值,小于中位数的会选择删除极大值,仅需特判中位数的选择即可确定本轮删除的对象。

于是模拟上述过程,根据区间长度维护中位数即可。

Code by nebulyu.

#include<bits/stdc++.h>
#define ffor(i,a,b) for(int i=a;i<=b;++i)
using namespace std;
using ll=long long;
using pli=pair<ll,int>;
const int N=1e6+5;
ll ar[N],n;
pli rec[N];
void solve(){
	cin>>n;
	ffor(i,1,n)cin>>ar[i],rec[i]=pli(ar[i],i);
	sort(rec+1,rec+1+n);
	ffor(i,1,n)ar[i]=rec[i].first;
	int l=1,r=n;
	while(l+1<r){
		ll mid=(ar[l]+ar[r])/2;
		int p=upper_bound(ar+1,ar+n+1,mid)-ar;
		int lp=p-l,rp=r-p+1;
		if(rp>lp)++l;else --r;
	}
	cout<<rec[l].second;
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	solve();
	return 0;
} 

J

给定整数 \(n, m\),要求构造一数列 \(a\),满足:

  • \(a\) 的长度为 \(n\)
  • \(\forall 1\le i\le n, -m\le a_i\le m\)
  • \(\forall 1\le l< r\le n, \sum_{i=l}^{r} a_i \ge 0\)

求满足条件的数列 \(a\) 的种类数,答案对 998244353 取模。
\(1\le n,m\le 5000\)
1S,512MB。

先膜拜 nebulyu。

显然不可能出现连续两个负数,并且对于一个合法的数列,其中负数两侧一定均为正数,且大于该负数的绝对值。

赛时我想到的做法是设 \(f_{i,j}\) 表示最小后缀和为 \(j\) 的合法前缀 \(1\sim i\) 的方案数,显然有 \(-m\le j\le m\),状态数至多只有 \(2\times n\times m\) 个。初始化 \(f_{1, j}:=1\),转移时考虑在最后填入一个数的影响:

考虑枚举添加后的最小后缀和 \(j\)。若在最后添加一个正数,则添加后最小后缀和一定不小于 0,否则不合法。则添加前最小后缀和的范围显然为 \([j - m, m]\);若在最后添加一个负数,则添加后的最小后缀和一定小于 0 且为该负数,为保证合法,添加前最小后缀和的范围显然为 \([-j, m]\)

则有:

\[f_{i, j} = \begin{cases} &\sum\limits_{k=j-m}^{m} f_{i - 1, k} &(j \ge 0)\\ &\sum\limits_{k = -j}^{m} f_{i - 1, k} &(j < 0) \end{cases}\]

答案即 \(\sum_{j = -m}^{m} f_{n, j}\)

维护一个后缀和转移即可。实现时可以直接把 \(f\) 的第一维省去。总时间复杂度 \(O(nm)\) 级别。

//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#define LL long long
const LL p = 998244353;
const int kN = 5e3 + 10;
const int d = 5e3;
//=============================================================
int n, m;
LL f[kN << 1], suf[kN << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  n = read(), m = read();
  for (int i = d - m; i <= d + m; ++ i) f[i] = 1;
  for (int i = m + d; i >= d - m; -- i) suf[i] = suf[i + 1] + f[i];
  
  for (int i = 2; i <= n; ++ i) {
    for (int j = 0; j <= m; ++ j) f[d + j] = suf[d + j - m];
    for (int j = 1; j <= m; ++ j) f[d - j] = suf[d + j];
    for (int j = d + m; j >= d - m; -- j) suf[j] = (suf[j + 1] + f[j]) % p;
  }
  printf("%lld\n", suf[-m + d]);
  return 0;
}

H

\(n\times n\) 个排列成方阵的边长为 1 的小正方形,给定一种操作:

  • 选择以 \((x, y)\) 为左上角,边长为 \(k\) 的正方形区域,将将整个区域合并为一个大正方形。
  • 合并前区域内的正方形个数 \(c\) 需要满足 \(2\le c\le 50\)

要求构造一种操作方案,操作次数长度任意,使得经过若干次操作后得到一个 \(n\times n\) 的大正方形。
\(1\le n\le 1000\)
1S,512MB。

哈,居然不是纯构造,居然是 tama 的乱搞题。

对于一个大小为 \(k\times k\) 的正方形区域,显然当 \(k\le 7\) 时可以直接合并;当 \(k>7\) 时考虑四分解:将其分为左上角的 \(a\times a\) 的正方形、右下角的 \(k - a\times k - a\) 的正方形以及两侧 \(a\times k - a\) 的矩形。递归地考虑,左上角和右下角的较小正方形都可以合并成一个,仅需关注两侧 \(a\times k - a\) 的矩形区域可不可以分成至多 24 个正方形区域。

考虑一种贪心的分解方法,每次从矩形区域较长边 \(p\) 的一端分出一份边长为较短边 \(q\) 的正方形,直到被分出的正方形边长 \(\le 7\)。发现这个过程就是辗转相减,至多拆出 \(\left\lfloor \frac{p}{q} \right\rfloor\),然后再递归地拆分 \((p\bmod q)\times q\) 的矩阵,据此就可以通过辗转相除法计算拆分出的数量是否 \(\le 24\)

嘛,虽然不能保证对于每一个 \(a\),两侧 \(a\times k - a\) 的矩形区域均可以分成至多 24 个正方形区域,不过可以枚举 \(a\) 再对每个 \(a\times k - a\) 进行上述类似辗转相除法的算法,只要能找到一个 \(a\) 使得上述条件成立即可,说明找到了 \(k\times k\) 正方形区域的一种分解方法,之后就可以递归解决问题了。

于是考虑顺序枚举所有满足 \(1\le k\le n\)\(k\times k\) 正方形区域,都对它们进行上述过程并得到它们的一种四分解方案,再据此 DfS + 辗转相减构造 \(n\times n\) 的方案即可。

总时间复杂度上界大概是 \(O(n^2\log (n^2))\) 级别。

//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <vector>
#include <cstring>
#include <algorithm>
const int kN = 1e3 + 10;
//=============================================================
int n, f[kN];
struct operation {
  int a, b, c;
};
std::vector <operation> ans;
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
bool Check(int x_, int y_) {
  if (!y_) return x_ <= 7;
  int cnt = 0, temp;
  while (y_) {
    cnt += x_ / y_;
    temp = y_, y_ = x_ % y_, x_ = temp;
  }
  return cnt <= 24;
}
void Init() {
  n = read();
  for (int i = 1; i <= n; ++ i) f[i] = -1;

  f[1] = 0;
  for (int i = 1; i <= n; ++ i) {
    for (int j = 0; j <= i / 2; ++ j) {
      if (Check(i - j, j)) {
        f[i] = j;
        break;
      }
    }
  }
}
void Dfs(int, int, int);
void Solve2(int, int, int, int);
void Solve1(int x_, int y_, int w_, int h_) {
  if (w_ <= 1) return ;
  for (int i = 0; i < h_ / w_; ++ i) Dfs(x_, y_ + i * w_, w_);
  Solve2(x_, y_ + (h_ / w_) * w_, w_, h_ % w_);
}
void Solve2(int x_, int y_, int w_, int h_) {
  if (h_ <= 1) return ;
  for (int i = 0; i < w_ / h_; ++ i) Dfs(x_ + i * h_, y_, h_);
  Solve1(x_ + (w_ / h_) * h_, y_, w_ % h_, h_);
}
void Dfs(int x_, int y_, int h_) {
  if (h_ == 1) return ;
  ans.push_back((operation) {x_, y_, h_});
  if (f[h_] == 0) return ;
  int h1 = h_ - f[h_], h2 = f[h_];
  Solve1(x_ + h1, y_, h2, h1), Solve2(x_, y_ + h1, h1, h2);
  Dfs(x_, y_, h1), Dfs(x_ + h1, y_ + h1, h2);
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  Init();
  Dfs(1, 1, n);
  printf("%d\n", ans.size());
  for (int i = ans.size() - 1; i >= 0; -- i) {
    printf("%d %d %d\n", ans[i].a, ans[i].b, ans[i].c);
  }
  return 0;
}

I

给定一 \(n\) 个节点的有向完全图,单向边 \(<u, v>\) 边权为 \(a_{u, v}\)。给定一长度为 \(k\) 的数列 \(v\),表示一个人初始时位于节点 \(v_1\),之后需要按顺序访问 \(v_2, v_3, \cdots, v_k\)。现在可以选择两个节点进行操作,令它们之间的两条单向边权值均置零,求这个人所经过路程之和的最小值。
\(2\le n\le 500\)\(2\le k\le 10^6\)\(\forall 1\le i,j\le n, 0\le a_{i, j}\le 10^9\)\(\forall 1\le i\le n, 1\le v_i\le n\)
5S,512MB。

牛逼题。

先跑个 Floyd 把未置零时两点间最短路跑出来,把按顺序访问 \(v_1\sim v_k\) 看成经过某两点间的最短路若干次,求得此时的路程之和 \(S\)

考虑一个暴力,枚举被操作的两个点 \((u, v)\),设 \(\operatorname{f}_{u, v}\) 表示置零有向边 \(<u, v>\) 的影响。考虑置零 \(<u,v>\) 后对某次移动 \((s, t)\) 的影响,钦定最短路经过 \(<u, v>\) 时长度为 \(\operatorname{dis}(s, u) + \operatorname{dis}(v, t)\),则答案的减小量为 \(\operatorname{dis}(s, t) -\operatorname{dis}(s, u) - \operatorname{dis}(v, t)\),则 \(f_{u, v} = \max\left(0, \operatorname{dis}(s, t) -\operatorname{dis}(s, u) - \operatorname{dis}(v, t)\right)\)

于是可以考虑暴力枚举 \(u, v, s, t\) 并处理出 \(f\),答案即:

\[S -\max_{1\le u< v\le n }\{ f_{u, v} + f_{v, u}\} \]

复杂度 \(O(n^4)\) 级别,会寄的很彻底。\(n\le 500\),最多只能枚举三个,怎么办呢?考虑是否可以通过单调性来省去一层循环,或者考虑双指针来同时枚举两个。

观察 \(f_{u, v} = \max\left(0, \operatorname{dis}(s, t) -\operatorname{dis}(s, u) - \operatorname{dis}(v, t)\right)\),考虑先枚举 \(s, t\),再按 \(\operatorname{dis}(v, t)\) 升序来枚举 \(v\),那么有贡献的 \(u\) 一定是满足 \(\operatorname{dis}(s, t) - \operatorname{dis}(v, t)\ge \operatorname{dis}(s, u)\) 的,即所有有贡献的 \(u\) 可以看做是按 \(\operatorname{dis}(s, u)\) 升序排列后的一段不断缩小的前缀,即可得到对于某次移动 \((s, t)\),固定了置零的边 \(<u, v>\) 终点 \(v\) 后有贡献的 \(u\) 的范围。于是可以使用双指针来求得有贡献的排序后最靠后的 \(u\) 的位置,再通过差分来进行前缀的修改。

于是考虑对于每一个 \(s\) 预处理按 \(\operatorname{dis}(s, u)\) 升序排列的 \(u\) 组成数列 \(\operatorname{Id}_s\),对于每一个 \(t\) 预处理预处理按 \(\operatorname{dis}(v, t)\) 升序排列的 \(t\) 组成数列 \(\operatorname{Id}_t\)。然后枚举 \(s\),同时维护两个数组 \(\operatorname{d}_{v, i}, \operatorname{c}_{v, i}\) 分别表示以 \(v\) 为置零的边终点,以 \(i\) 为被置零的边的起点为 \(\operatorname{Id}_{s}(i)\) 时,有贡献的移动 \((s, t)\) 的影响 \(\operatorname{dis}(s, t) - \operatorname{dis}(v, t)\) 之和,以及有贡献的移动 \((s, t)\) 的数量。两个数组可以通过上述差分修改再前缀和还原得到。

在对某个 \(s\) 完成上述操作,再枚举 \(v\)\(i\) 计算对 \(f\) 的贡献即可。比较难以描述,这里仅解释数组含义,具体过程详见代码。

总复杂度为 \(O(n^3 + n^2\log n+k)\) 级别。

//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <vector>
#include <cstring>
#include <algorithm>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 510;
const LL kInf = 1e15 + 2077;
//=============================================================
int n, k, a[kN][kN];
std::vector <pr <LL, int> > disvt[kN], dissu[kN];
LL dis[kN][kN], delta[kN][kN], cnt[kN][kN], d[kN][kN], c[kN][kN];
LL sum;
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
//dis(s, t) - dis(s, u) - dis(v, t);
void Floyd() {
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      dis[i][j] = a[i][j];
    }
  }
  for (int k = 1; k <= n; ++ k) {
    for (int i = 1; i <= n; ++ i) {
      for (int j = 1; j <= n; ++ j) {
        dis[i][j] = std::min(dis[i][j], dis[i][k] + dis[k][j]);
      }
    }
  }
}
void Init() {
  n = read(), k = read();
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      a[i][j] = read();
    }
  }
  Floyd();
  int u = read();
  for (int i = 2; i <= k; ++ i) {
    int v = read();
    sum += dis[u][v];
    ++ cnt[u][v];
    u = v;
  }
}
void Solve() {
  for (int t = 1; t <= n; ++ t) {
    for (int v = 1; v <= n; ++ v) {
      disvt[t].push_back(mp(dis[v][t], v));
    }
    std::sort(disvt[t].begin(), disvt[t].end());
  }
  for (int s = 1; s <= n; ++ s) {
    for (int u = 1; u <= n; ++ u) {
      dissu[s].push_back(mp(dis[s][u], u));
    }
    std::sort(dissu[s].begin(), dissu[s].end());
  }
  
  for (int s = 1; s <= n; ++ s) {
    for (int i = 1; i <= n; ++ i) {
      for (int j = 0; j < n; ++ j) {
        d[i][j] = c[i][j] = 0;
      }
    }
    for (int t = 1; t <= n; ++ t) {
      if (!cnt[s][t]) continue;
      for (int i = 0, j = n - 1; i < n; ++ i) {
        int v = disvt[t][i].second;
        LL d1 = dis[s][t] - dis[v][t];
        while (0 <= j && dissu[s][j].first >= d1) -- j;
        if (j == -1) break;
        d[v][j] += cnt[s][t] * d1;
        c[v][j] += cnt[s][t];
      }
    }
    for (int v = 1; v <= n; ++ v) {
      for (int i = n - 1; i >= 0; -- i) {
        int u = dissu[s][i].second;
        d[v][i] += d[v][i + 1], c[v][i] += c[v][i + 1];
        delta[u][v] += d[v][i];
        delta[u][v] -= c[v][i] * dis[s][u]; 
      }
    }
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  Init();
  Solve();
  LL ans = sum;
  for (int i = 1; i <= n; ++ i) {
    for (int j = i + 1; j <= n; ++ j) {
      ans = std::min(ans, sum - delta[i][j] - delta[j][i]);
    }
  }
  printf("%lld\n", ans);
  return 0;
}

写在最后

学到了什么:

  • 别惦记你那填了 \(i\) 个数最后一个数是 \(j\) 了。尝试用题目中特有的对答案有影响的因素设 DP 状态。
  • 别惦记你那几把人类智慧构造了。
  • 没有单调性就自己造。
posted @ 2023-07-29 11:07  Luckyblock  阅读(40)  评论(0)    收藏  举报