首页 > 技术文章 > 2020.11.31 选拔赛题解

DreamW1ngs 2020-12-04 06:22 原文

训练赛地址

A (线段树or暴力)

题目链接
⭐⭐⭐

题意:
有一个长为\(L\)的停车道,坐标从0开始,车辆停放时车头朝向正方向,且距离车头\(f\)范围内和距离车尾\(b\)范围内不能有其它车辆。
有两种操作:

  1. “1 \(a\)”,有1辆长度为\(a\)的车开进来找停车位。如果能找到输出最小的起始位置,否则输出-1.
  2. “2 \(a\)”,第\(i\)个操作中开进来的车开出去了(保证第\(i\)个操作是1操作)

解析:

  • 做法一(线段树)
    考虑建立线段树,维护区间内最大无车区域\(mx\),左端点向内最大无车区域\(ls\),右端点向内最大无车区域\(rs\),以及\(lazy\)标签
  1. 对于\(update\)函数,用\(lazy\)维护维护区间修改值,,进行基础的区间修改即可,当修改区间完全被包含在线段区间内,同时更新\(mx,ls,rs\)
  2. 对于\(query\)函数,考虑左端点向内无车区域、左区间+右区间无车区域,右端点向内无车区域三种放置车辆情况
    • 对于左端点向内无车区域可以放置的情况,对左区间进行递归查询
    • 对于左区间+右区间可以放置的情况,返回【左区间右端点-对应的\(rs\)值】
    • 对于右端点向内无车区域可以放置的情况,对右区间进行递归查询
  3. 对于\(pushdown\)函数,\(lazy\)标签下移,更改维护区段值
  4. 对于\(pushup\)函数,父区间\(ls\)承接左区间\(ls\),父区间\(rs\)承接右区间\(rs\),且如果左区间都可用,则可以延伸到右区间的\(ls\),同理,如果右区间都可用,则可以延伸到左区间的\(rs\),而父区间对应的\(mx\),则要满足\(mx=\max(mx_l,mx_r,rs_l+ls_r)\)
  5. 由于整个\(L\)两端的汽车放置不受\(b,f\)的影响,因此可以将\(L\)拓展\(L+b+f\),这样处理还避免了\(query\)查询中,如果初始整个区间都要被使用的情况。
    注意:
    1. \(query\)函数中,一定要按所给顺序进行查询,保证是在最左端插入可放置车辆
    2. 对于返回值而言,由于整体\((b+a+f)\)插入位置比线段树位置少了\(b\)\(L\)改变所导致),而车尾位置又比整体插入位置多了b,相抵,便可以得到线段树位置即为车辆车尾对应的位置
#include<bits/stdc++.h>
using namespace std;
/*===========================================*/

struct Tree
{
	int l, r, lazy, ls, rs, mx;
};

const int maxn = 1e6 + 205;

struct Segment_Tree
{
	Tree tree[maxn << 2];

	void pushdown(int root)
	{
		if (~tree[root].lazy)
		{
			tree[root << 1].mx = tree[root << 1].ls = tree[root << 1].rs = (tree[root << 1].r - tree[root << 1].l) * tree[root].lazy;
			tree[root << 1 | 1].mx = tree[root << 1 | 1].ls = tree[root << 1 | 1].rs = (tree[root << 1 | 1].r - tree[root << 1 | 1].l) * tree[root].lazy;
			tree[root << 1].lazy = tree[root << 1 | 1].lazy = tree[root].lazy;
			tree[root].lazy = -1;
		}
	}
	void pushup(int root)
	{
		tree[root].ls = tree[root << 1].ls, tree[root].rs = tree[root << 1 | 1].rs;
		tree[root].mx = max(tree[root << 1].mx, tree[root << 1 | 1].mx);
		tree[root].mx = max(tree[root].mx, tree[root << 1].rs + tree[root << 1 | 1].ls);
		if (tree[root << 1].mx == tree[root << 1].r - tree[root << 1].l)
			tree[root].ls += tree[root << 1 | 1].ls;
		if (tree[root << 1 | 1].mx == tree[root << 1 | 1].r - tree[root << 1 | 1].l)
			tree[root].rs += tree[root << 1].rs;
	}

	void build(int k, int l, int r)
	{
		tree[k].l = l, tree[k].r = r;
		tree[k].ls = tree[k].rs = tree[k].mx = r - l;
		tree[k].lazy = -1;
		if (r - l != 1)
			build(k << 1, l, l + (r - l) / 2), build(k << 1 | 1, l + (r - l) / 2, r);
	}

	void update(int k, int ql, int qr, int val)
	{
		int& l = tree[k].l, & r = tree[k].r, & mx = tree[k].mx, & ls = tree[k].ls, & rs = tree[k].rs;
		if (ql <= l && r <= qr)
		{
			ls = rs = mx = (r - l) * val;
			tree[k].lazy = val;
		}
		else if (ql<r && qr>l)
		{
			pushdown(k);
			update(k << 1, ql, qr, val);
			update(k << 1 | 1, ql, qr, val);
			pushup(k);
		}
	}

	int query(int k, int val)
	{
		int& l = tree[k].l, & r = tree[k].r, & mx = tree[k].mx, & ls = tree[k].ls, & rs = tree[k].rs;
		if (tree[k << 1].mx >= val)
		{
			pushdown(k);
			return query(k << 1, val);
		}
		else if (tree[k << 1].rs + tree[k << 1 | 1].ls >= val)
			return tree[k << 1].r - tree[k << 1].rs;
		else if (tree[k << 1 | 1].mx >= val)
		{
			pushdown(k);
			return query(k << 1 | 1, val);
		}
		return -1;
	}

}sol;

pair<int, int> qu[105];

int main()
{
	//freopen("abc.in", "r", stdin);
	int L, b, f;
	int n, opt, a;
	while (~scanf("%d%d%d", &L, &b, &f))
	{
		L += b + f;
		sol.build(1, 0, L);
		scanf("%d", &n);
		for (int i = 1; i <= n; ++i)
		{
			scanf("%d%d", &opt, &a);
			if (opt == 1)
			{
				int ans = sol.query(1, a + b + f);
				printf("%d\n", ans);
				if (ans == -1) continue;
				qu[i].first = ans + b;
				qu[i].second = qu[i].first + a;
				sol.update(1, qu[i].first, qu[i].second, 0);
			}
			else
				sol.update(1, qu[a].first, qu[a].second, 1);
		}
	}
}
  • 做法二(暴力排序枚举)
    查看题目可以发现,询问次数竟然只有最多100次?What?! 也就是说停车场中的车辆也最多只有100辆,于是这道题可以直接暴力
  1. 由于端点车辆放置不受\(b,f\)的影响,所以提前在数组中放置\([-b,-b]\)\([L+f,L+f]\)的两辆汽车(这样不用特判)
  2. 遇到1操作,\(O(n)\)查询可以最先可以插入的位置,然后进行插入,插入位置\(i\)需要满足

\[car[i].l-car[i-1].r>=a+b+f \]

同时需要保存对应的操作数,操作结束后进行排序
3. 遇到2操作,\(O(n)\)查询\(a\)所对应的车辆,进行删除即可

#include<bits/stdc++.h>
using namespace std;
/*===========================================*/
struct Car
{
	int id;
	int l, r;
	bool operator<(const Car& a)const
	{
		return l < a.l;
	}
};

vector<Car> car;

void solve()
{
	int L, b, f, n;
	while (~scanf("%d%d%d", &L, &b, &f))
	{
		car.clear();
		car.push_back(Car{ -1, -b, -b });
		car.push_back(Car{ -1,L + f,L + f });
		int opt, a;
		scanf("%d", &n);
		for (int j = 1; j <= n; ++j)
		{
			scanf("%d%d", &opt, &a);
			if (opt == 1)
			{
				bool ok = false;
				for (int i = 1; i < car.size(); ++i)
				{
					if (car[i].l - car[i - 1].r >= a + b + f)
					{
						car.push_back(Car{ j,car[i - 1].r + b,car[i - 1].r + b + a });
						printf("%d\n", car[car.size() - 1].l);
						ok = true;
						sort(car.begin(), car.end());
						break;
					}
				}
				if (!ok)
					printf("-1\n");
			}
			else
				for (int i = 1; i < car.size(); ++i)
					if (car[i].id == a)
					{
						car.erase(car.begin() + i);
						break;
					}
		}
	}
}

int main()
{
	solve();
}

B(dfs构造)

题目链接
⭐⭐

题意:
给你三个整数,\(n,d,k\)。你的任务是重新构建出一棵树或者判断出不能构造出这样的树。\(n\)代表这棵树一共有\(n\)个点,\(d\)代表的是这棵树的直径为\(d\)\(k\)是每个最大节点度数为\(k\)。如果能够构造出这样的树,输出‘YES’,和所有的边。如果不能,那么输出‘NO’。

解析:

  1. 首先构造出这条最长的直径,接下来就是向这条直径上的点上添加其他的点同时保证直径不会超过\(d\)即可
  2. 很明显可以发现,添加点的层数\(i\)要保证\(i\le \min(i-1,d+1-i)\)
  3. 这样的话便可以用dfs进行指数级别的递归构造

注意:

  1. 第一次进行构造时,由于是从直径上的点开始,所以可用度为\(k-2\),而往后的构造可用度为\(k-1\)
  2. \(k=1\wedge n>2\text{(only two points exist is allowed)}\qquad and \qquad n\le d\text{(construct the diam)}\)两种情况进行特判
  3. 递归里写法一定要注意先后顺序,保证在达到目标数以后,不再向数组中添加新答案,也不要继续递归性查询,否则会爆MLE
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;

int now;
typedef pair<int, int> P;
vector<P> v;
int n, d, k;

void dfs(int fa, int up, int lim)
{
	if (now == n || !up || !lim) return;
	while (up--)
	{
		v.push_back(P(fa, ++now));
		if (now == n) return;
		else dfs(now, k - 1, lim - 1);
		if (now == n) return;
	}
}

int main()
{
	scanf("%d%d%d", &n, &d, &k);
	if (k == 1 && n > 2 || n <= d)
	{
		printf("NO");
		return 0;
	}
	bool ok = false;
	now = d + 1;
	for (int i = 1; i <= d; ++i)
		v.push_back(P(i, i + 1));
	for (int i = 2; i <= d; ++i)
		dfs(i, k - 2, min(i - 1, d + 1 - i));
	if (now == n)
	{
		printf("YES\n");
		for (auto& i : v)
			printf("%d %d\n", i.first, i.second);
	}
	else
		printf("NO");

}

C(二分+贪心)

题目链接
⭐⭐

题目:
给出\(n\)个数,从中选出长度为\(k\)的子序列形成环,如果相邻两个元素之和不超过\(M\),求解\(M_{min}\)

解析:

  1. 对于两个数,如果他们的和不超过\(M\),则二者至少有一个小于等于\(\lfloor\frac{M}{2}\rfloor\)
  2. 那么便可以记录这些\(x\)的位置\(i\ ,\{i\mid p[i]\le\lfloor\frac{M}{2}\rfloor\}\)贪心地在\((p[i],p[i-1])\)之间插入可行点,这样就可以获得对于某个\(M\)所能得到的环内元素数量\(\Delta k\)的最大值
  3. 可以得到每个\(M\)所对应的\(\Delta k_{max}\),便可以进行二分

注意:

  1. 由于单个数字上限达到了\(10^9\),所以\(M\)会大于\(2\times10^9>INT_{max}\),因此一定要开long long
#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=2e5+5;
int w[maxn];
int p[maxn];
int n,k;

int ok(long long m)
{
    int res=0;
    int cnt=0;
    for(int i=0;i<n;++i)
        if(w[i]<=m/2)
            p[cnt++]=i;
    if(cnt<=1)
        return cnt;
    for(int i=1;i<cnt;++i)
        for(int j=p[i-1]+1;j<p[i];++j)
            if(1ll*w[j]+max(w[p[i-1]],w[p[i]])<=m)
            {
                ++res;
                break;
            }
    for(int j=p[cnt-1]+1;j<n;++j)
        if(1ll*w[j]+max(w[p[cnt-1]],w[p[0]])<=m)
            return res+cnt+1;
    for(int j=0;j<p[0];++j)
        if(1ll*w[j]+max(w[p[cnt-1]],w[p[0]])<=m)
            return res+cnt+1;
    return res+cnt;
}

int main()
{
    scanf("%d%d",&n,&k);
    for(int i=0;i<n;++i)
        scanf("%d",&w[i]);
    long long l=0,r=2e9+10,mid;
    while(l<r)
    {
        mid=l+(r-l)/2;
        if(ok(mid)>=k) r=mid;
        else l=mid+1;
    }
    printf("%lld",r);
}

D(打表找规律)

题目链接

题意:
给出¥1,¥5,¥10,¥50,四种钞票,问\(n\)张钞票可以组成多少种不同的面额

解析:
初看题目很像一道dp,但发现\(n\)高达\(10^9\),果断放弃
打表找规律不失为一种好方法

注意:不开long long成神仙

总结:
通过打表发现在\(n>11\)时,答案是一个等差数列,公差为49,这样的话对\(n\le11\)部分进行特判,就解决了问题

打表代码:

#include<bits/stdc++.h>
using namespace std;
/*===========================================*/

set<int> s;

void dfs(int x, int cur, int num)
{
	switch (x)
	{
	case 0:
		for (int i = 0; i <= cur; ++i)
			dfs(x + 1, cur - i, num + i);
		break;
	case 1:
		for (int i = 0; i <= cur; ++i)
			dfs(x + 1, cur - i, num + 5 * i);
		break;
	case 2:
		for (int i = 0; i <= cur; ++i)
			dfs(x + 1, cur - i, num + 10 * i);
		break;
	case 3:
		s.insert(num + cur * 50);
	}
}

#include<bits/stdc++.h>
using namespace std;
/*===========================================*/

LL ans[] = { 0,4,10,20,35,56,83,116,155,198,244,292 };

int main()
{
	LL n;
	scanf("%lld", &n);
	if (n <= 11)
		printf("%lld", ans[n]);
	else
		printf("%lld", ans[11] + 49 * (n - 11));
}

E(暴力+KMP)

题目链接
⭐⭐⭐

题意:
给出\(n\)个独立的单词。在任意两个相邻的单词之间有且仅有一个空格。需要注意的是,在第一个单词之前没有空格,在最后一个单词之后也没有空格。
现在可以进行一次操作,将重复出现过的单词以及他们之间的空格进行组合,形成一个单词组,每个单词用一个字母表示,例如"\(ab\ cd\ ab\ cd\ ee\rightarrow A A\ ee\)",这个单词组在原所给单词序列中出现需要多于一次
问进行这样的操作后,求单词序列长度的最小值

解析:

  1. \(map\)存储每个单词所对应的编码,这样就将单词组,压缩成了一个只包含数字的数组。同时记录每个编码所对应单词的长度\(sz\)
  2. 暴力枚举区间\([l,r]\),对区间内的编码进行KMP预处理,查询这段编码在整体单词序列中出现的次数\(cnt\)
  3. 那么如果出现次数大于1的情况下,写出对应的单词组替代情况

\[\underbrace{\overbrace{word_1}^{sz_1}\ word_2\ \cdots\ word_n}_{\sum_{i=1}^nsz_i(words)+n-1(space)} \]

可以发现减少了\((\sum_{i=1}^nsz_i)+n-1\),而增加了单词数量所对应的代表字符\(n\),所以总的来说减少了\((\sum_{i=1}^nsz_i)-1\),对结果统计最小值即可

#include<bits/stdc++.h>
#define IOS std::ios::sync_with_stdio(0);cin.tie(0);
using namespace std;
/*===========================================*/

map<string, int> m;
const int maxn = 305;
int sz[maxn];
int a[maxn];
int nxt[maxn];
int cnt;


int main()
{
	IOS
	int n, res = 0;
	string s;
	cin >> n;
	for (int i = 0; i < n; ++i)
	{
		cin >> s;
		if (!m[s]) m[s] = ++cnt, sz[cnt] = s.size();
		a[i] = m[s];
		res += s.size();
	}
	res += n - 1;
	int total = res;
	for (int lf = 0; lf < n; ++lf)
	{
		int len = 0;
		for (int ri = lf; ri < n; ++ri)
		{
			len += sz[a[ri]];
			nxt[0] = -1;
			int cnt = 0;
			int i = 0, k = -1;
			while (i < ri - lf)
			{
				if (k == -1 || a[lf + i] == a[lf + k])
				{
					if (a[lf + ++i] == a[lf + ++k]) // 当两个字符相等时要跳过
						nxt[i] = nxt[k];
					else
						nxt[i] = k;
				}
				else
					k = nxt[k];
			}
			nxt[ri - lf + 1] = 0;
			i = 0; // 主串的位置
			int j = 0; // 模式串的位置
			while (i < n)
			{
				if (j == -1 || a[i] == a[lf + j])
				{
					++i, ++j;
					if (j == ri - lf + 1)
					{
						++cnt;
						j = nxt[j];
						//操作
					}
				}
				else
					j = nxt[j];
			}
			if (cnt != 1)
				res = min(res, total - cnt * (len - 1));
		}
	}
	cout << res;
}

推荐阅读