10月20日模拟赛题解

10月20日模拟赛题解

A 纸牌

Description

桌面上有 \(n\) 张纸牌,每张纸牌的正反两面各写着一个整数,初始时正面朝上。现在要求你翻动最少的纸牌,使得朝上的数字中最少有一半的数字是相同的,或判断无解。

Limitations

\(1 \leq n \leq 3 \times 10^5\),所有输入数据都是不大于 \(10^9\) 的非负整数。

Solution

签到题,注意到因为要求至少有一半的数字相同,并且一共只有 \(2n\) 个数字,所以最终可以作为相同数字的数不超过 \(4\) 个。用 map/hash 随便维护一下出现次数,然后枚举超过一半的数字,暴力判断即可。

Code

#include <cstdio>
#include <map>
#include <vector>

const int maxn = 300005;

int n, ans = maxn, dn;
int a[maxn], b[maxn];
std::map<int, int> oc;
std::vector<int> ansv;

int main() {
  freopen("card.in", "r", stdin);
  freopen("card.out", "w", stdout);
  qr(n); dn = (n >> 1) + (n & 1);
  for (int i = 1; i <= n; ++i) {
    qr(a[i]); qr(b[i]);
    if (a[i] != b[i]) {
      if (++oc[a[i]] == dn) {
        ansv.push_back(a[i]);
      }
      if (++oc[b[i]] == dn) {
        ansv.push_back(b[i]);
      }
    } else {
      if (++oc[a[i]] == dn) {
        ansv.push_back(a[i]);
      }
    }
  }
  if (ansv.size() == 0) {
    puts("Impossible");
    return 0;
  }
  for (auto v : ansv) {
    int cnt = 0;
    for (int i = 1; i <= n; ++i) {
      cnt += a[i] == v;
    }
    ans = std::min(ans, dn - cnt);
  }
  printf("%d\n", std::max(0, ans));
}

B 后缀树组

Description

给定一个长度为 \(n\) 的字符串,对每个位置 \(i\) ,取它和它后面 \((m - 1)\) 个字符共 \(m\) 个字符作为第 \(i\) 个子串。(如果到达结尾,则子串长度为 \(n - i + 1\))。现在将这 \(n\) 个子串按照第一个字符所在的位置排成一排,要将他们按照字典序排序,每次只能交换相邻字符串,求最少交换次数。

Limitations

对于全部的数据,\(1 \leq m \leq n \leq 50000\),字符串只含小写字母

对于前 \(60\%\) 的数据,\(1 \leq n \leq 5000\)

另有 \(10\%\) 的数据, \(1 \leq m \leq 5\)

另有 \(10\%\) 的数据,字符串随机生成。

Solution

首先的结论是,将一个序列按照不降序排序且只能交换相邻两项,则最优的交换次数是这个序列的逆序对数。证明上可以考虑先将最大的元素移动到序列末尾,所需要的移动次数是该最大元素所贡献的逆序对个数,然后去掉序列末尾元素,对剩下的序列继续排序,以此做数学归纳即可。

因此只要知道每个子串的字典序排名,我们就可以 \(O(n \log n)\) 的求出答案。

考虑前 \(60\%\) 的数据,可以 \(O(nm)\) 的找出每个子串,然后排序,用冒泡去 \(O(n^2)\) 的求出逆序对个数。不过大概只有 zxy 这个 sb 会拿冒泡去求逆序对了(

对于 \(m \leq 5\) 的数据,排好序用 BIT 或者 mergesort 求一下就好了。

对于字符串随机的数据,我们注意到在比较字典序的时候,第一个字符相同从而进入下一位比较的概率是 \(\frac{1}{25 \times 26}\),再进行一位比较的概率是上面这个概率的平方,类似的,我们发现每次期望比较的次数非常小,不会超过 \(5\),因此排序的时间复杂度就是 \(O(nT \log n)\),其中 \(T\) 是字符串随机意义下期望的两串比较次数。但是注意到把所有的字串都求出来会爆空间,所以直接在原串上扫就行了。不够大概只有 zxy 这个 铁憨憨 会把所有的子串都求出来叭(

对于全部的数据,我们考虑比较两个字符串字典序的过程,从前往后扫两个串的前缀,只要有某个长度使得两个串在该长度对应字符不同,就可以通过这个字符来比较两个串的字典序,因此我们考虑找到两个字符串的第一个不同的前缀。而这个前缀的长度是可以二分的,即若找到了某个长度使得两个串的该长度前缀长度不同,第一个不同的前缀的长度一定不大于这个长度,否则第一个不同的前缀长度一定大于这个长度。

而判断两个串的前缀是否相同可以用HASH来解决。这样就可以做到时间复杂度 \(O(n \log n \log m)\) 了,其中 \(O(\log m)\) 是二分的复杂度。

至于怎么求一个优秀的与字符顺序有关但是与字符位置无关(因为要求 \(ab\)\(ba\) 不同,要求与顺序有关,但是如 \(abab\),要求判断前两个字符组成的字符串与后两个字符组成的字符串相同,要求与字符位置无关)的 hash 函数,可以对原串 hash 一遍,然后对原串的差分 hash 一遍,然后对原串的二阶差分 hash 一遍,一直 hash 下去就好了(

Code

(80 pts)

#include <cstdio>
#include <algorithm>

typedef long long int ll;

const int maxn = 50005;

int n, m, ans;
int v[maxn], w[maxn];
char S[maxn];

struct BIT {
  int A[maxn];

  inline int lowbit(const int x) { return x & -x; }

  inline void update(int x, const int v) { do A[x] += v; while ((x += lowbit(x)) <= n); }

  inline int query(int x) { int _ret = 0; do _ret += A[x]; while (x -= lowbit(x)); return _ret; }
};
BIT tree;

bool cmp(const int &a, const int &b);

int main() {
  freopen("sort.in", "r", stdin);
  freopen("sort.out", "w", stdout);
  scanf("%d %d\n%s", &n, &m, S + 1);
  for (int i = 1; i <= n; ++i) { v[i] = i; }
  std::sort(v + 1, v + 1 + n, cmp);
  for (int i = 1; i <= n; ++i) {
    w[v[i]] = i;
  }
  for (int i = 1; i <= n; ++i) {
    ans += tree.query(n) - tree.query(w[i]);
    tree.update(w[i], 1);
  }
  qw(ans, '\n', true);
  return 0;
}

inline bool cmp(const int &a, const int &b) {
  for (int len = 1, i = a, j = b; len <= m; ++i, ++j, ++len) {
    if ((i > n) || (j > n)) {
      return i > n;
    } else if (S[i] != S[j]) {
      return S[i] < S[j];
    }
  }
  return a < b;
}

(std)

#include <cstdio>

#define mo 1000000007
#define N 50055

int f[N],s[N],tmp[N],n,m,i,ch,ans;
long long hash[N],pow[N];

//二分+哈希求以i开头的和以j开头的两个子串哪个字典序更小
bool lessThanOrEqual(int i, int j)
{
	if (i == j) return true;
	int l, r, k;
	long long hsi, hsj;
	//二分求i和j开始从左向右第一位不同的位
	l = 0;
	r = m+1;
	if (n-j+2 < r) r = n-j+2;
	if (n-i+2 < r) r = n-i+2;
	while (r-l > 1)
	{
		k = (l+r)/2;
		//子串[i,i+k-1]的哈希值
		hsi = hash[i+k-1]-hash[i-1]*pow[k]%mo;
		if (hsi < 0) hsi += mo;
		//子串[j,j+k-1]的哈希值
		hsj = hash[j+k-1]-hash[j-1]*pow[k]%mo;
		if (hsj < 0) hsj += mo;
		if (hsi == hsj) l = k; else r = k;
	}
	//s[i+l]和s[j+l]是第一位不同的位
	if (l == m) return true;
	return s[i+l] < s[j+l];
}

//归并排序
void sort(int l, int r)
{
	if (l == r) return;
	int mi = (l+r)/2;
	sort(l, mi);
	sort(mi+1, r);
	int i=l, j=mi+1;
	int nt = l;
	while (i<=mi || j<=r)
	{
		bool ilej;
		if (i > mi) ilej = false;
		else
		if (j > r) ilej = true;
		else ilej = lessThanOrEqual(f[i],f[j]);
		if (ilej) tmp[nt++] = f[i++];
		else
		{
			tmp[nt++] = f[j++];
			//从右区间取数时,右区间和左区间之间产生了继续对
			//累加答案
			ans += mi-i+1;
		}
	}
	for (i=l; i<=r; ++i) f[i] = tmp[i];
}

int main()
{
	freopen("sort.in", "r", stdin);
	freopen("sort.out", "w", stdout);
	scanf("%d%d", &n, &m);
	hash[0] = 0;
	pow[0] = 1;
	for (i=1; i<=n; ++i)
	{
		for (ch=getchar(); ch<=32; ch=getchar());
		s[i] = ch-96;
		//预处理hash[i]=子串[1,i]的哈希值
		hash[i] = (hash[i-1]*29+s[i])%mo;
		//预处理pow[i]=29^i
		pow[i] = pow[i-1]*29%mo;
		f[i] = i;
	}
	s[n+1] = 0;
	sort(1, n);
	printf("%d\n", ans);
	return 0;
}

C 巧克力

有一块分成 \(n \times m\) 个格子的矩形巧克力,虽然形状上很规整但质量分布并不均匀,每一格有各自的重量 \(w_{i, j}\),用 \(n \times m\) 个正整数表示。你需要将这一整块巧克力切成 \(k\) 小块,要求每块都是矩形,且它们的重量分别为 \(a_1 \sim a_k\)。一块巧克力的重量等于它包含的所有格子的重量之和。

切巧克力的时候,你可以每次选一块大的巧克力,沿着某条格线横向或纵向将其切成两块小的巧克力。切下来的小块巧克力可以继续切割。切割路线不能是折线或斜线。任何时候当前的所有巧克力块都必须是矩形的。

对于给定的巧克力和分割要求,请你判断是否存在一个切割方案满足上述要求。

共有 \(T\) 组数据,时限 \(2s\)

Limitations

img

Solution

Algorithm \(1\)

判断一下 a 加起来是否等于 \(m\),当 \(w\) 恒等于 \(1\) 且只有一行的时候,只要按照 \(a\) 去一个一个切即可。

可过测试点:\(1\)。期望得分 \(10~pts\)

Algorithm \(2\)

爆搜切几刀从哪里切,注意到每切一块都会有一块新的巧克力产生,因此最多切 \(k\) 刀,而对于每块巧克力,都只有 \(O(n + m)\) 种切法,因此搜索树的深度为 \(k\),每个节点有 \((n + m)\) 个孩子。爆搜的复杂度为 \(O(T~(n + m) ^ k)\)

可通过测试点:\(1,~2,~3,~4\),期望得分 \(40~pts\)

Algorithm \(3\)

zxy 那个 铁憨憨 一样读错题,以为每切一刀都必须满足一个 \(a\),然后写个垃圾爆搜,也能得到 \(40 pts\)

Algorithm \(4\)

注意到 \(k\) 非常小,因此非常适宜状压。

\(f_{i, j, x, y, S}\) 为左上角为 \((i, j)\),右下角为 \((x,~y)\) 的矩形,是否满足 \(a\) 的状态为 \(S\) 的情况,转移只要枚举那一刀在哪里切得满足了哪些情况即可。写成记搜非常好写。

时间复杂度 \(O(T n^2 m^2 (n + m) 3^k)\)。空间复杂度 \(O(n^2m^2 \times 3^k)\)

可通过测试点:\(1~\sim 6\),期望得分 \(60~pts\)

Algorithm \(5\)

注意到复杂度的瓶颈在状态数上,考虑优化状态。

我们发现对于一个确定了左上角和右上角的矩形,如果再确定了它要满足的 \(a\) 之和,那么他的左下角和右下角就可以确定了。因此我们发现只要确定了 \(i,~j,~x\)\(S\),那么 \(y\) 就可以被确定了,因此在搜索的时候将矩形和不等于 \(S\) 状态下 \(a\) 之和的状态剪掉,那么搜到的状态数就变成了 \(O(n^2 m \times 2^k)\)

于是这样的时间复杂度 \(O(T \times n^2 \times m\times (n + m) \times 3^k)\)

可通过测试点:\(1~\sim 8\),期望得分 \(80~pts\)

Algorithm \(6\)

对于 \(w = 1\) 的点,我们注意到相当于拿一些小矩形拼成这样一个大矩形。由于各个小矩形完全相同,我们不需要记录具体该矩形是第几行第几列。因此可以设 \(f_{i, j, S}\) 是长为 \(i\),宽为 \(j\) 的矩形,能否拼出状态为 \(S\)\(a\),转移时依然可以枚举这一刀是怎么切的。

时间复杂度 \(O(n \times m \times (n + m) \times 3^k)\)

可通过测试点:\(1,~3,~5,~7,~9\),期望得分 \(50~pts\)

Algorithm \(7\)

注意到在转移的时候,我们已经枚举了转移到哪个集合,那么我们就不再需要去枚举从哪里切这一刀,因为a的和是确定的,竖向和横向都最多只有一种切刀的方法,具体在哪里切这一刀,可以二分这个位置。这样转移的复杂度就被优化到了 \(O(\log m)\)。总时间复杂度 \(O(n^2 m 3^k \log m)\)。可以通过全部的测试点。

期望得分 \(100~pts\)

Code

(80分)

#include <cstdio>
#include <cstring>

typedef long long int ll;

const int maxn = 11;
const int maxt = 1030;

bool vis[maxn][maxn][maxn][maxn][maxt], frog[maxn][maxn][maxn][maxn][maxt];

int n, m, k, T;
int MU[maxn][maxn], A[maxn], sum[maxn][maxn], val[maxt];

void work();
void clear();
bool dfs(const int x, const int y, const int z, const int w, const int S);

int main() {
  freopen("chocolate.in", "r", stdin);
  freopen("chocolate.out", "w", stdout);
  qr(T);
  while (T--) {
    clear();
    work();
  }
  return 0;
}

void clear() {
  memset(A, 0, sizeof A);
  memset(MU, 0, sizeof MU);
  memset(val, 0, sizeof val);
  memset(vis, 0, sizeof vis);
  memset(sum, 0, sizeof sum);
  memset(frog, 0, sizeof frog);
  n = m = k = 0;
}

void work() {
  qr(n); qr(m); qr(k);
  for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= m; ++j) {
      qr(MU[i][j]);
      sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + MU[i][j];
    }
  }
  for (int i = 0; i < k; ++i) {
    qr(A[i]);
  }
  int ALL = (1 << k) - 1;
  for (int S = 1; S <= ALL; ++S) {
    for (int i = 0; i < k; ++i) if (S & (1 << i)) {
      val[S] += A[i];
    }
  }
  puts(dfs(1, 1, n, m, ALL) ? "yes" : "no");
}

bool dfs(const int x, const int y, const int z, const int w, const int S) {
  bool &thisv = vis[x][y][z][w][S], &thisf = frog[x][y][z][w][S];
  if (thisv) { return thisf; }
  thisv = true;
  if ((sum[z][w] - sum[x - 1][w] - sum[z][y - 1] + sum[x - 1][y - 1]) != val[S]) {
    return false;
  }
  if ((S & (S - 1)) == 0) {
    return thisf = true;
  }
  for (int i = x; i < z; ++i) {
    for (int S0 = S; S0; S0 = (S0 - 1) & S) if (dfs(x, y, i, w, S0) && dfs(i + 1, y, z, w, S ^ S0)) {
      return thisf = true;
    }
  }
  for (int i = y; i < w; ++i) {
    for (int S0 = S; S0; S0 = (S0 - 1) & S) if (dfs(x, y, z, i, S0) && dfs(x, i + 1, z, w, S ^ S0)) {
      return thisf = true;
    }
  }
  return false;
}

(std)

#include <cstdio>
#include <list>

#define MAXK 15
#define N 11

struct Quad
{
	int a, b, c, d;
	Quad(int _a, int _b, int _c, int _d): a(_a), b(_b), c(_c), d(_d) {}
};

std::list<Quad> lf, lfx, lfy;
char f[N][N][N][1<<MAXK],fx[N][N][N][1010],fy[N][N][N][1010];
int sumx[N][N][N],sumy[N][N][N],suma[1<<MAXK],bg[1<<MAXK],ed[1<<MAXK],c[15000000],e[MAXK+1],a[20],
    n,m,K,i,j,l,r,T,sta,nc,w;

/*
求:以j1为左边界、j2为右边界、i1为上边界的矩形中,下边界为多少的矩形
重量和是w。如果不存在则返回-1
用二分求
*/
int calcx(int j1, int j2, int i1, int w)
{
	// fx[j1][j2][i1][w]用于记录该子问题有没有被求结果
	// 已求结果则直接返回结果
	if (fx[j1][j2][i1][w] != 0) return fx[j1][j2][i1][w];
	// 未求结果,将该状态加入待清空队列
	lfx.push_back(Quad(j1,j2,i1,w));
	// 二分求i2的位置
	int l, r, k;
	l = i1-1;
	r = n+1;
	while (r-l > 1)
	{
		k = l+r>>1;
		if (sumx[j1][j2][k]-sumx[j1][j2][i1-1] <= w) l = k; else r = k;
	}
	if (sumx[j1][j2][l]-sumx[j1][j2][i1-1] != w) l = -1;
    return fx[j1][j2][i1][w]=l;
}

/*
求:以i1为上边界、i2为下边界、j1为左边界的矩形中,右边界为多少的矩形
重量和是w。如果不存在则返回-1
和上面对称
*/
int calcy(int i1, int i2, int j1, int w)
{
	if (fy[i1][i2][j1][w] != 0) return fy[i1][i2][j1][w];
	lfy.push_back(Quad(i1,i2,j1,w));
	int l, r, k;
	l = j1-1;
	r = m+1;
	while (r-l > 1)
	{
		k = l+r>>1;
		if (sumy[i1][i2][k]-sumy[i1][i2][j1-1] <= w) l = k; else r = k;
	}
	if (sumy[i1][i2][l]-sumy[i1][i2][j1-1] != w) l = -1;
    return fy[i1][i2][j1][w]=l;
}

/*
求(i1,j1)~(i2,j2)的矩形能否切出sta中的巧克力
*/
bool work(int i1, int i2, int j1, int j2, int sta)
{
	//记忆化:求过了则直接返回
	if (f[i1][i2][j1][sta] != 0) return f[i1][i2][j1][sta]==1;
	if (bg[sta] == ed[sta]) return true;
	//未求过,将该状态加入待清空队列
	lf.push_back(Quad(i1,i2,j1,sta));
	int i, sta2, x, y;
	//枚举sta的每个非空真子集
	for (i=bg[sta]; i<ed[sta]; ++i)
	{
		sta2 = c[i];
		
		//尝试横向切
		x = calcx(j1,j2,i1,suma[sta2]);
		if (x != -1)
		if (work(i1,x,j1,j2,sta2) && work(x+1,i2,j1,j2,sta-sta2))
		{
			f[i1][i2][j1][sta] = 1;
			return true;
		}
		
		//尝试纵向切
		y = calcy(i1,i2,j1,suma[sta2]);
		if (y != -1)
		if (work(i1,i2,j1,y,sta2) && work(i1,i2,y+1,j2,sta-sta2))
		{
			f[i1][i2][j1][sta] = 1;
			return true;
		}
	}
	f[i1][i2][j1][sta] = -1;
	return false;
}

void dfs(int sta, int t)
{
	if (t == MAXK)
	{
		if (sta > 0) c[nc++] = sta;
		return;
	}
	if (sta&e[t]) dfs(sta-e[t], t+1);
	dfs(sta, t+1);
}

int main()
{
	freopen("chocolate.in", "r", stdin);
	freopen("chocolate.out", "w", stdout);
	e[0] = 1; 
	for (i=1; i<=MAXK; ++i) e[i] = e[i-1]*2;
	
	//预处理每个sta有哪些非空真子集,连续存储在队列c中
	nc = 1;
	for (sta=1; sta<e[MAXK]; ++sta)
	{
		bg[sta] = nc; //bg表示sta的子集在c中的开头位置
		dfs(sta, 0); //dfs求sta的非空真子集
		--nc;
		ed[sta] = nc; //ed表示sta的子集在c中的结尾位置
	}
	
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d%d%d", &n, &m, &K);
		for (i=1; i<=n; ++i)
		for (j=1; j<=m; ++j)
		{
			scanf("%d", &w);
			//sumy[i][j][k]:从第i行到第j行,从第1列到第k列构成的矩形的重量和
			sumy[i][i][j] = sumy[i][i][j-1]+w;
			//sumx[i][j][k]:从第i列到第j列,从第1行到第k行构成的矩形的重量和
			sumx[j][j][i] = sumx[j][j][i-1]+w;
		}
		for (l=1; l<n; ++l)
		for (r=l+1; r<=n; ++r)
		for (j=1; j<=m; ++j) sumy[l][r][j] = sumy[l][r-1][j]+sumy[r][r][j];
		for (l=1; l<m; ++l)
		for (r=l+1; r<=m; ++r)
		for (i=1; i<=n; ++i) sumx[l][r][i] = sumx[l][r-1][i]+sumx[r][r][i];
		
		for (i=1; i<=K; ++i) scanf("%d", &a[i]);
		//求出{ai}的各个子集的重量和
		//suma[sta]:sta中的巧克力的总重量
		for (sta=0; sta<e[K]; ++sta)
		{
			suma[sta] = 0;
			for (i=sta, j=1; i>0; i>>=1, ++j)
			if (i&1) suma[sta] += a[j];
		}
		
		// 如果所有ai的总重量!=巧克力的总重量
		if (suma[e[K]-1] != sumy[1][n][m])
		{
			printf("no\n");
			continue;
		}
		
		//lf、lfx、lfy用于记录哪些状态被记忆化了,用于之后清零
		lf.clear();
		lfx.clear();
		lfy.clear();
		
		if (work(1,n,1,m,e[K]-1)) printf("yes\n");
		else printf("no\n");
		
		//清零记忆化过的状态
		for (std::list<Quad>::iterator it=lf.begin(); it!=lf.end(); ++it) f[it->a][it->b][it->c][it->d] = 0;
		for (std::list<Quad>::iterator it=lfx.begin(); it!=lfx.end(); ++it) fx[it->a][it->b][it->c][it->d] = 0;
		for (std::list<Quad>::iterator it=lfy.begin(); it!=lfy.end(); ++it) fy[it->a][it->b][it->c][it->d] = 0;
	}
	return 0;
}

posted @ 2019-10-21 23:41  一扶苏一  阅读(626)  评论(0编辑  收藏  举报