有道小图灵青少年编程精英挑战活动 中学组 第4场 题解

Here

T1 题解

我们可以发现,到某一个方格距离小于等于某个值的所有方格构成一个斜着的正方形

子任务1

直接每局时间,并前缀和处理,可以算出在规定时间内的警察个数

子任务2

二分答案,并沿用子任务1的做法

T2 题解

我们将两种操作分别简称为阅读/卖出。

子任务1:

暴力枚举操作序列。暴力枚举每次是卖出还是阅读。

时间复杂度:\(O(q*n!*2^n)\)

子任务2:

考虑如何处理单次询问。考虑贪心。

首先,每次选择阅读时,必定会选择当前能选择的书当中最便宜的那个。每次卖出书籍时,由于卖出书籍得到的收益与被卖出书籍的价格无关,所以选择当前能选择的书当中最贵的那个必定最优,因为这样我们就可以把更便宜的书留个以后的购买。

因此我们可以暴力枚举每一天究竟是卖出还是阅读,当确定了是卖出还是阅读后,我们就可以唯一确定操作对象,这样就省去了枚举操作对象的序列。

时间复杂度:\(O(q*2^n)\)

子任务3:

首先我们将\(p\)数组从小到大排序。

假设我们已经决定了在\(m\)次操作中阅读\(k\)本书。

注意到,我们会尽量想推迟卖出一本书的时间,因为卖出的越晚,卖出时阅读过的书籍数量越多,可以拿到的钱就更多。

因此,最优的策略一定是,首先卖出尽量少的书籍凑够购买第\(1\)本书的钱,然后阅读第一本书,再卖出尽量少的书籍凑够购买第\(2\)本书的书籍,再阅读第\(2\)本书,以此类推,直到购买完\(k\)本书籍,再在余下回合中弃掉。

枚举k,我们可以得到一个时间复杂度为\(O(q*n^2)\)的算法。

接下来我们考虑优化这个贪心。

设我们原本希望购买\(k\)本书,现在我们打算卖掉第\(k+1\)本书,假设此时\(k+1\)会给我们带来\(k+2\)个金币;但假如我们决定阅读第\(k+1\)本书而不是把它卖掉,我们将花费$p _ {k+1} \(个金币,但我们会多获得等于剩余卡牌数量的金币,设为t。因此我们只需要判断若\)k+2 \leq -p _ { k+1 } + t\(则可以取\)k+1$。

时间复杂度:\(O(nq)\)

子任务4:

发现\(k\)\(m\)的增长是单调递增的。可以用这种理解方式,即书的总数量越多,阅读一本书的收益就越大,因此阅读的书至少不减。

于是我们可以从前往后扫,并维护\(k\)的指针,每次判断能不能后移,最多后移\(n\)次。

时间复杂度:\(O(n\log n+q\log q)\)\(\log\)来自排序。

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn=200005;	

typedef long long ll;

ll n;
ll a[maxn];

struct Query
{
	int m;
	int id;
}t[maxn];

bool cmp(Query x, Query y)
{
	if(x.m==y.m) return x.id<y.id;
	else return x.m<y.m;
}

ll ans[maxn];

ll cel(ll x, ll y)
{
	if(x%y==0) return x/y;
	else return x/y+1;
}

int main()
{
//	freopen("card20.in","r",stdin);
//	freopen("card20.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	sort(a+1,a+n+1);
	int q; cin>>q;
	for(int i=1;i<=q;i++)
	{
		cin>>t[i].m;
		t[i].id=i;
	}
	sort(t+1,t+q+1,cmp);
	
	ll p=0, lst=0;
	ll prof=0;
	ll pbf=0;
	
	t[0].m=0;
	for(int i=1;i<=q;i++)
	{
		prof+=(t[i].m-t[i-1].m)*(2+p);
		int m=t[i].m,req,te,delt;
		while(p<n)
		{
			te=p+1;
			req=(pbf>=a[te]?0:(cel((a[te]-pbf),(2+p))));
			if(req+lst+p+1>n) break;
			delt=-a[te]-(2+p)+(t[i].m-p-1-req-lst);
		//	cout<<"queue "<<p<<' '<<req<<' '<<delt<<' '<<-a[te]-(2+p)<<' '<<(t[i].m-p-1-req-lst)<<' '<<lst<<' '<<req<<' '<<t[i].m<<endl;
			if(delt>=0)
			{
				pbf+=((2+p)*req-a[te]);
				p++;
				lst+=req;
				prof+=delt;
			}
			else break; 	
		}
		ans[t[i].id]=prof;
	}
	for(int i=1;i<=q;i++) cout<<ans[i]<<endl;
	return 0;	
}

T3 题解

首先来进行一步转换,把“这种饼干第一次出现的下标”转换成“这种饼干是第几个出现的”(个人认为比较好处理,也方便理解)

由于这题给出一个数组求第几个出现,所以很自然地想到可以用类似数位DP的套路来做

那么令 \(dp_{i,j,0/1}\) 代表遍历到第 \(i\) 个位置,出现了 \(j\) 中饼干,有没有首位限制的数列方案数(\(0\) 代表没有限制,\(1\) 代表有)

那么转移如下:

\(dp_{i,j,0} * j \rightarrow dp_{i+1,j,0}\)

\(dp_{i,j,1} * (a_{i+1}-1) \rightarrow dp_{i+1,j,0}\)(不管 \(a_{i+1}\) 是否比 \(j\) 大,在这里都不能取)

\(dp_{i,j,0} \rightarrow dp_{i+1,j+1,0}\)

考虑有限制的情况

如果 \(a_{i+1} > j\)

\(dp_{i,j,1} \rightarrow dp_{i+1,j+1,1}\)

否则 \(dp_{i,j,1} \rightarrow dp_{i+1,j,1}\)

初始化 \(dp_{1,1,1}=1\),因为显然第一个位置只能是 \(1\)

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

typedef long long ll;

ll read()
{
	char c = getchar(); ll ans = 0, f = 1;
	while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
	while(c >= '0' && c <= '9') {ans = ans * 10 + c - '0'; c = getchar();}
	return ans * f;
}

const int INF = 1e9;
const int MAXN = 5010;
const int MOD = 1e9 + 7;
ll dp[2][MAXN][2], b[MAXN], a[MAXN];
ll n, ans = 1, cnt;

inline ll get(ll x) {return (x >= MOD) ? (x - MOD) : ((x < 0) ? (x + MOD) : x);}
inline ll add(ll a, ll b) {return get(a + b);}
inline ll sub(ll a, ll b) {return get(a - b);}
inline ll mul(ll a, ll b) {return a * b % MOD;}

int main()
{
	n = read();
	for(int i = 1; i <= n; ++ i) b[i] = read();
	for(int i = 1; i <= n; ++ i)
	{
		if(b[i] == i) a[i] = ++ cnt;
		else a[i] = a[b[i]];
	}
	dp[1][1][1] = 1;
	for(int i = 1; i < n; ++ i)
	{
		int cur = i & 1, nxt = (i + 1) & 1;
		for(int j = 1; j <= i; ++ j)
		{
			dp[nxt][j][0] = add(dp[nxt][j][0], add(mul(dp[cur][j][0], j), mul(dp[cur][j][1], a[i + 1] - 1)));
			dp[nxt][j + 1][0] = add(dp[nxt][j + 1][0], dp[cur][j][0]);
			if(a[i + 1] <= j) dp[nxt][j][1] = add(dp[nxt][j][1], dp[cur][j][1]);
			else dp[nxt][j + 1][1] = add(dp[nxt][j + 1][1], dp[cur][j][1]);
			dp[cur][j][0] = dp[cur][j][1] = 0;
		}
	}
	for(int i = 1; i <= n; ++ i) ans = add(ans, dp[n & 1][i][0]);
	printf("%lld\n", ans);
	return 0;
}

T4 题解

前置知识:线段树(部分分)、单调栈、单调队列、STL set 的使用、重构思想。

无解显然是存在一个值 \(>Y\) 的数。

子任务 1:

肯定能把整个分成一段,答案就是所有权值和。

子任务 2:

\(s_i\)\(A\) 的前缀和。

考虑 DP,\(f_{i}\) 表示把 \(1 \sim i\) 分成若干段最优解,转移为 \(\displaystyle f_i = \min_{j<i, i - j \le X, s_i - s_j \le Y}\{f_j + \max_{k=j+1}^i(A_k)\}\)

暴力 DP 是 \(O(n^3)\) 的。

子任务 3:

上一步用个线段树搞搞?

子任务 4:

里面的 \(\max\) 用 ST 表预处理一下,或者第二维倒着扫,动态更新那坨,都是 \(O(n^2)\) 的。

子任务 5:

留给不知道是啥的。。

子任务 6:

考虑用数据结构暴力优化 DP。

首先对于一个 \(i\),合法的决策 \(j\) 是个区间,可以双指针线性获得。

\(f_j + \max_{k=j+1}^i(A_k)\) 东西,设 \(c_j\) 等于这个东西,再设 \(w_j = \max_{k=j+1}^i(A_k)\)

\(c_j = f_j + w_j\)。考虑用线段树动态维护这个 \(c\) 数组,每次 DP 取合法决策区间的最大值即可。

\(f\) 插入就不变了,考虑 \(w\) 如何变,加入 \(A_i\) 后,设 \(p_i\) 为在 \(i\) 左侧第一个比 \(A_i\) 大的数(这个可以线性单调栈),\(A_i\) 影响的范围即 \([p_i, i - 1]\),在线段树上区间覆盖即可。

时间复杂度 \(O(n \log n)\) 但由于常数比较大貌似过不了 \(n = 10^6\)。(可能也可以过,不太清楚)

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
#define MAXN 3000100
#define ll long long

inline ll read()
{
	ll x=0;char c=getchar();
	while (c<'0'||c>'9') c=getchar();
	while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
	return x;
}

ll n, m, cnt, a[MAXN], q[MAXN], fi[MAXN], len, Sum, sum[MAXN], dp[MAXN], now;

struct node
{
	ll left, right, lazy, Min;
	node *ch[2];
}pool[4*MAXN], *root;
inline void buildtree(node *p, ll left, ll right)
{
	p->left = left; p->right = right; p->Min = 1000000000;
	if(left == right) return ;
	ll mid = (left + right) / 2;
	node *lson = &pool[++cnt], *rson = &pool[++cnt];
	p->ch[0] = lson; p->ch[1] = rson;
	buildtree(lson, left, mid); buildtree(rson, mid+1, right);
}
inline void push(node *p)
{
	if(p->lazy == 0) return ;
	if(p->ch[0]) p->ch[0]->lazy += p->lazy;
	if(p->ch[1]) p->ch[1]->lazy += p->lazy;
	p->Min += p->lazy;
	p->lazy = 0;
}
inline void change1(node *p, ll left, ll right, ll s)
{
	if(p->left == left && p->right == right)
	{
		p->lazy += s;
		return ;
	}
	if(p->ch[0]->right >= right) change1(p->ch[0], left, right, s);
	else if(p->ch[1]->left <= left) change1(p->ch[1], left, right, s);
	else
		change1(p->ch[0], left, p->ch[0]->right, s), change1(p->ch[1], p->ch[1]->left, right, s);
	if(p->ch[0]) push(p->ch[0]);
	if(p->ch[1]) push(p->ch[1]);
	p->Min = min(p->ch[0]->Min, p->ch[1]->Min);
}
inline void change2(node *p, ll left, ll right, ll s)
{
	if(p->left == left && p->right == right)
	{
		p->Min = s;
		return ;
	}
	if(p->ch[0]->right >= right) change2(p->ch[0], left, right, s);
	else if(p->ch[1]->left <= left) change2(p->ch[1], left, right, s);
	p->Min = min(p->ch[0]->Min, p->ch[1]->Min);
}
inline ll query(node *p, ll left, ll right)
{
	push(p);
	if(p->left == left && p->right == right) return p->Min;
	if(p->ch[0]->right >= right) return query(p->ch[0], left, right);
	else if(p->ch[1]->left <= left) return query(p->ch[1], left, right);
	else
		return min(query(p->ch[0], left, p->ch[0]->right), query(p->ch[1], p->ch[1]->left, right));
}
void dfs(node *p)
{
	push(p);
	if(p->left == p->right)
	{
		printf("%lld ",p->Min);
		return ;
	}
	dfs(p->ch[0]);
	dfs(p->ch[1]);
}
int main()
{
	scanf("%lld%lld%lld",&n,&len,&Sum);
	ll mx=0, u, v, first, last;
	for(ll i=1;i<=n;i++) a[i] = read(), mx = max(mx, a[i]), sum[i] = sum[i-1] + a[i];
	if(mx > Sum)
	{
		printf("No Solution\n");
		return 0;
	}
	dp[1] = a[1];
	q[1] = 1;
	root = &pool[++cnt];
	buildtree(root, 0, n);
	change2(root, 0, 0, 0);
	first = 1; last = 0;
	for(ll i=1;i<=n;i++)
	{
		while(sum[i] - sum[now] > Sum || i-now > len) now++;
		q[++last] = i;
		fi[i] = i-1;
		change1(root, i-1, i-1, a[i]); 
		while(first < last && a[q[last-1]] < a[q[last]]) 
		{
			change1(root, fi[q[last-1]], fi[q[last]]-1, a[q[last]]-a[q[last-1]]);
			fi[q[last]] = fi[q[last-1]];
			q[last-1] = q[last];
			last--;
		}
//		dfs(root);
		dp[i] = query(root, now, i-1);
		change2(root, i, i, dp[i]);
//		cout<<endl;
	}
//	for(ll i=1;i<=n;i++) printf("%lld ",dp[i]);
	printf("%lld\n",dp[n]);
	
	return 0;
}

子任务 7

能数据结构硬优化的已经尽可能优化了,没有什么优化空间。

考虑分析出一些性质,我们排除一些不必要的决策。

首先由一个性质 \(f_{i-1} \le f_{i}\) ,比较显然,明显把最后一段最后一个数去掉答案不会增加。

着眼于最后一段,考虑这个决策 \(j\),如果这个 \(A_j\) 加入不会把 \(w\) 这个东西改变,也就他不是 \([j, i]\) 的最大值,并且加入这个数最后一段不会超出长度、和的限制,那么就可以把这个纳入最后一段,这个决策就不如 \(j - 1\)

因此,一个决策 \(j\) 可能成为 \(f_i\) 的最优决策仅当:

  • \(A_j\)\(A_{j \sim i}\) 的最大值。
  • \(j\) 是可行的决策区间最靠左的一个。

第二个决策只有一个暴力转移即可。

考虑第一个决策,事实上就是维护一个 \(A\) 单调递减的单调队列(为了保持选的段满足长度限制还得踢出队头)。

考虑在插入 \(A_{1 \sim i}\) 进单调队列后,单调队列除了最后一项外,都是可能的决策点。考虑他们的 \(w\) 是什么,你可以 \(\text{ST}\)\(O(1)\) 求,也可以分析性质,此时一个决策对应的 \(w\) 就是单调队列的上的下一个位置的 \(A\) 值。

考虑转移,你需要知道这些决策对应的 \(c\) 值的最大值,因此你需要支持一个这样一个双端插入删除求最值的数据结构,操作 \(O(n)\) 次:

  • 尾部插入
  • 首尾删除
  • 求最大值

可以再开一个 \(\text{multiset}\),插入删除对应在这个上面操作即可。这样是单次 \(\log n\) 的。

时间复杂度还是 \(O(n \log n)\)

事实上由于每次维护的决策的队列都不满,所以卡不满,可以获得 \(80\) 分。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <set>
#define int long long
using namespace std;
typedef long long LL;
const int N = 3000005;
int n, a[N], q[N];
LL m, t, s[N], f[N];
multiset<LL> st;
LL inline g(int x) { return f[q[x]] + a[q[x + 1]]; }

void inline read(int &x) {
	x = 0; char s = getchar();
	while (s < '0' || s > '9') s = getchar();
	while (s <= '9' && s >= '0') x = (x << 1) + (x << 3) + (s ^ 48), s = getchar();
}

signed main() {
	scanf("%lld%lld%lld", &n, &t, &m);
	for (int i = 1; i <= n; i++) {
		read(a[i]), s[i] = s[i - 1] + a[i];
		if (a[i] > m) { puts("No Solution"); return 0; }
	}
	int j = 0, hh = 1, tt = 0;
	for (int i = 1; i <= n; i++) {
		while (j + 1 < i && (s[i] - s[j] > m || i - j > t)) j++;
		while (hh < tt && (s[i] - s[q[hh]] > m || i - q[hh] > t)) st.erase(st.find(g(hh++)));
		while (hh <= tt && a[q[tt]] <= a[i]) {
		    if (hh < tt) st.erase(st.find(g(tt - 1)));
		    tt--;
		}
		q[++tt] = i;
		if (hh < tt) st.insert(g(tt - 1));
		f[i] = f[j] + a[q[hh]];
		if (hh < tt) f[i] = min(f[i], *st.begin());
	}
	printf("%lld\n", f[n]);
	return 0;
}

子任务 8

考虑子任务 \(7\) 所说的数据结构。如果单端删除我们可以用单调队列做到线性,这是因为我们建立了一个时间删除的先后顺序让我们可以踢掉”比你小还比你强“这样的东西使其单调,那双端插入删除能不能做到呢?

这里引入一个不知道叫啥的数据结构,就叫双调栈吧,他可以支持均摊 \(O(1)\) 每次两端插入删除求最值:

  • 考虑设置一个中点 \(mid\),假设每次删除不会越过 \(mid\),左右维护两个栈。顺着这个假设,如果说我比你靠近中间,还比你小,那么你就没用了,就可以不插入。这样就做到了让两个栈两端小,中间大做到单调。
  • 如果删除越过 \(mid\),你就考虑把当前存在的元素都暴力重构,\(mid\) 还是取中位数。

这样复杂度为啥是对的呢?考虑一次暴力重构,意味从上次重构到现在,有着 \(\frac{mid}{2}\) 量级,也就是和 \(mid\) 同阶的数遭到了删除,你的重构复杂度与这个同阶。而总共删除的数是 \(O(n)\) 的,因此可以做到总共复杂度线性 \(O(n)\)

#include <cstdio>
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
typedef long long LL;
const int N = 3000005;
int n, a[N], q[N], mid, hh = 1, tt;
LL m, t, s[N], f[N], A[N], B[N], t1, t2, val[N];
LL inline g(int x) { return f[q[x]] + a[q[x + 1]]; }

void inline read(int &x) {
	x = 0; char s = getchar();
	while (s < '0' || s > '9') s = getchar();
	while (s <= '9' && s >= '0') x = (x << 1) + (x << 3) + (s ^ 48), s = getchar();
}

void inline pushA(int x) {
	if (!t1 || val[A[t1]] > val[x]) A[++t1] = x;
}

void inline pushB(int x) {
	if (!t2 || val[B[t2]] > val[x]) B[++t2] = x;
}

void inline rebuild() {
	t1 = t2 = 0;
	mid = (hh + tt) >> 1;
	for (int i = mid; i >= hh; i--) pushA(i);
	for (int i = mid + 1; i < tt; i++) pushB(i);
}

int main() {
	scanf("%d%d%lld", &n, &t, &m);
	for (int i = 1; i <= n; i++) {
		read(a[i]), s[i] = s[i - 1] + a[i];
		if (a[i] > m) { puts("No Solution"); return 0; }
	}
	int j = 0;  
	for (int i = 1; i <= n; i++) {
		while (j + 1 < i && (s[i] - s[j] > m || i - j > t)) j++;
		while (hh < tt && (s[i] - s[q[hh]] > m || i - q[hh] > t)) {
			if (t1 && A[t1] == hh) t1--;
			hh++;
			if (hh >= mid) rebuild();
			
		}
		while (hh <= tt && a[q[tt]] <= a[i]) {
		    if (t2 && B[t2] == tt - 1) t2--;
		    tt--;
		    if (tt <= mid) rebuild();
		}
		q[++tt] = i;
		if (hh < tt) {
			val[tt - 1] = g(tt - 1);
			pushB(tt - 1);
		}
		f[i] = f[j] + a[q[hh]];
		if (t1) f[i] = min(f[i], val[A[t1]]);
		if (t2) f[i] = min(f[i], val[B[t2]]);
	}
	printf("%lld\n", f[n]);
	return 0;
}
posted @ 2021-06-23 22:09  DMoRanSky  阅读(438)  评论(0编辑  收藏  举报