第十九届程协杯部分题解 A D F G I K

Preface

其实本来以为简单的题不少,会有很多人分数都很高,但是实际上来看也还好?(

貌似从完整过题率来看,区分度还是有的(虽然不是太明显)

有些题数据炸锅了,实在是抱歉,第一次负责出题,有些地方太弱智了实在抱歉。

以下是代码火车头,后续题解的std中就不会出现了:(这里有define int long long,所以后续的题解中都是int,没有long long)

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <unordered_map>
#include <iomanip>
#include <functional>
#include <random>
#define endl '\n'
#define int long long
#define rep(i,a,b) for (int aa = a, bb = b, i = aa; i <= bb; i++)
#define rep2(i,a,b) for (int aa = a, bb = b, i = aa; i >= bb; i--)
using namespace std;

template<typename T>
void cc(const vector<T> &tem) {
	for (const auto &x: tem) cout << x << ' ';
	cout << endl;
}

template<typename... Args>
void cc(const Args &... a) {
	size_t idx = 0;
	((std::cout << a << (++idx == sizeof...(Args) ? "\n" : " ")), ...);
}

void fileRead() {
#ifdef LOCALL
	freopen("D:\\AADVISE\\Clioncode\\untitled2\\in.txt", "r", stdin);
	freopen("D:\\AADVISE\\Clioncode\\untitled2\\out.txt", "w", stdout);
#endif
}

void kuaidu() { ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); }

inline int max(int a, int b) {
	if (a < b) return b;
	return a;
}

inline double max(double a, double b) {
	if (a < b) return b;
	return a;
}

inline int min(int a, int b) {
	if (a < b) return a;
	return b;
}

inline double min(double a, double b) {
	if (a < b) return a;
	return b;
}

void cmax(int &a, const int &b) { if (b > a) a = b; }
void cmin(int &a, const int &b) { if (b < a) a = b; }
void cmin(double &a, const double &b) { if (b < a) a = b; }
void cmax(double &a, const double &b) { if (b > a) a = b; }

int gcd(int a,int b) {
	if (!b) return a;
	return gcd(b, a % b);
}

template<const int T>
int Kuai(int a,int b) {
	int l = 1;
	while (b) {
		if (b % 2) l = l * a % T;
		a = a * a % T, b /= 2;
	}
	return l;
}

template<typename T>
using vec = vector<T>;
using PII = pair<int, int>;
using INT = __int128;

Problem A. 云端的数据结构

本来计算的是暴力可以拿到一半的分数,但是随机出来的,貌似整体偏小,再加上数据范围从5e5改成了1e5(怕很多人直接爆零),结果导致了暴力直接拿到了70分阿巴阿巴。

先说一下最暴力的写法,就是模拟的做法。预期得分25分。

每一次更新下标x,然后for循环套for循环,第一层循环枚举第i个点,第二层循环去求出来前i个点的最大值和后n-i个点的最大值,然后计算答案。

//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N];
//--------------------------------------------------------------------------------
//struct or namespace:

//--------------------------------------------------------------------------------

signed main() {
	fileRead();
	kuaidu();
	T = 1;
	//cin >> T;
	while (T--) {
		cin >> n;
		rep(i, 1, n) {
			cin >> A[i];
		}
		int q;
		cin >> q;
		rep(i, 1, q) {
			int x, k;
			cin >> x >> k;
			A[x] += k;
			int sum = 0;

			//枚举第j个点
			rep(j, 1, n) {
				int mmax1 = -INF, mmax2 = -INF;
				//找出前缀的最大值
				rep(p, 1, j) mmax1 = max(mmax1, A[p]);
				//找出后缀的最大值
				rep(p, j, n) mmax2 = max(mmax2, A[p]);
				//计算答案
				sum += min(mmax1, mmax2) - A[j];
			}
			cc(sum);
		}
	}
	return 0;
}

稍微不那么暴力的做法,就是开一个pre,suf数组,代表前缀1到i的a_i最大值和后缀i到n的a_i最大值。预期得分70分。

然后每一次更新了一个点x之后,我们用两个for循环分别去重新更新一下pre和suf数组,然后再用一个for循环来去求出每一个点的贡献。

这样的时间复杂度是O(qn)级别的,有意思的是看到赛时有人写的线段树,还以为写出来了正解,结果是用线段树去更新最大值,时间复杂度多了一个log。

//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N], pre[N], suf[N];
//--------------------------------------------------------------------------------
//struct or namespace:

//--------------------------------------------------------------------------------

signed main() {
	fileRead();
	kuaidu();
	T = 1;
	//cin >> T;
	while (T--) {
		cin >> n;
		rep(i, 1, n) {
			cin >> A[i];
			pre[i] = suf[i] = A[i];
		}
		rep(i, 1, n) pre[i] = max(pre[i], pre[i - 1]);
		rep2(i, n, 1) suf[i] = max(suf[i], suf[i + 1]);
		int q;
		cin >> q;
		rep(i, 1, q) {
			int x, k;
			cin >> x >> k;
			A[x] += k; //更新x
			//更新pre数组
			pre[x] = max(pre[x], A[x]);
			rep(j, x+1, n) pre[j] = max(pre[j], pre[j - 1]);
			
			//更新suf数组	
			suf[x] = max(suf[x], A[x]);
			rep2(j, x-1, 1) suf[j] = max(suf[j], suf[j + 1]);
			
			//求出每一个点的贡献
			int sum = 0;
			rep(j, 1, n) {
				sum += min(pre[j], suf[j]) - A[j];
			}
			cc(sum);
		}
	}
	return 0;
}

正解的办法是:

首先想到,这个问题求的是:

对于这个式子,我们可以化成

然后会发现,这个其实就是全局的最大值,我们用一个变量去维护就好了,然后关于a_i的部分也很方便就能维护出来(数组的和)。

那么如何可以快速地,在logn的复杂度下维护出来pre_i的和呢?(因为suf和pre很像,所以这里光说pre)

线段树,对数组进行维护,这个数组是代表前缀。如果修改了,变成那么就和当前线段树的这个位置比较,如果没有超过线段树维护这个点的,就不做任何操作。否则就要向右找到最后一个小于等于的位置(这一步我们可以用线段树上二分),这样我们就求出来了一个区间的左边界和右边界,然后把这段区间用区间赋值(懒标记)。

综上,线段树上的节点维护的信息有:(pre数组的求和),(懒标记),(代表区间最左端的pre,用来二分查找位置的),(记录区间长度)

开两个线段树直接暴力开搞。

//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------
//struct or namespace:
class SEG {
#define xl x+x
#define xr x+x+1

	//TODO 节点维护信息、apply函数、up函数
	struct info {
		int sum = 0;
		int siz = 1;
		int lan = 0; //懒标记
		int mmin = 0; //到达节点最左端的pre_mmax

		//区间加操作,这个题没有用到
		void apply1(int k) {
			sum += siz * k;
			lan += k;
			mmin += k;
		}

		//区间赋值操作
		void apply2(int k) {
			mmin = k;
			sum = siz * k;
			lan = k;
		}

		//up函数
		friend info operator+(const info &q1, const info &q2) {
			info q;
			q.siz = q1.siz + q2.siz;
			q.sum = q1.sum + q2.sum;
			q.mmin = min(q1.mmin, q2.mmin);
			return q;
		}
	};

	int L, R;
	vector<info> F;

	void init(int x, int l, int r) {
		if (l == r) {
			F[x] = info();
			return;
		}
		int mid = l + r >> 1;
		init(xl, l, mid), init(xr, mid + 1, r);
		F[x] = F[xl] + F[xr];
	}

	void down(int x) {
		if (!F[x].lan) return;
		F[xl].apply2(F[x].lan), F[xr].apply2(F[x].lan);
	}

	void add(int x, int l, int r, int l1, int r1, int k) {
		if (l1 > r1) return;
		if (l != r) down(x);
		F[x].lan = 0;
		if (l1 <= l and r <= r1) {
			F[x].apply1(k);
			return;
		}
		int mid = l + r >> 1;
		if (r1 <= mid) add(xl, l, mid, l1, r1, k);
		else if (mid < l1) add(xr, mid + 1, r, l1, r1, k);
		else add(xl, l, mid, l1, mid, k), add(xr, mid + 1, r, mid + 1, r1, k);
		F[x] = F[xl] + F[xr];
	}

	void add2(int x, int l, int r, int l1, int r1, int k) {
		if (l1 > r1) return;
		if (l != r) down(x);
		F[x].lan = 0;
		if (l1 <= l and r <= r1) {
			F[x].apply2(k);
			return;
		}
		int mid = l + r >> 1;
		if (r1 <= mid) add2(xl, l, mid, l1, r1, k);
		else if (mid < l1) add2(xr, mid + 1, r, l1, r1, k);
		else add2(xl, l, mid, l1, mid, k), add2(xr, mid + 1, r, mid + 1, r1, k);
		F[x] = F[xl] + F[xr];
	}

	//二分,找到小于等于val的最后一个位置
	int erfen(int x, int l, int r, int val) {
		int mid = l + r >> 1;
		if (l == r) return l;
		if (l != r) down(x);
		F[x].lan = 0;
		//如果右儿子的最小的那个值都小于等于我们修改后的val,那么就遍历右儿子,否则遍历左儿子。
		if (F[xr].mmin <= val) return erfen(xr, mid + 1, r, val);
		else return erfen(xl, l, mid, val);
	}

	info qry(int x, int l, int r, int l1, int r1) {
		if (l1 > r1) return info();
		if (l != r) down(x);
		F[x].lan = 0;
		if (l1 <= l and r <= r1) return F[x];
		int mid = l + r >> 1;
		if (r1 <= mid) return qry(xl, l, mid, l1, r1);
		else if (mid < l1) return qry(xr, mid + 1, r, l1, r1);
		else { return qry(xl, l, mid, l1, mid) + qry(xr, mid + 1, r, mid + 1, r1); }
	}
#undef xl
#undef xr

public:
	//TODO 调整乘的系数
	SEG(int l, int r) {
		L = l, R = r;
		F.resize(r * 2.7);
		init(1, l, r);
	}

	void clear(int l, int r) {
		L = l, R = r;
		init(1, l, r);
	}

	void add(int l, int r, int k) { add(1, L, R, l, r, k); }
	void add2(int l, int r, int k) { add2(1, L, R, l, r, k); }
	info qry(int l, int r) { return qry(1, L, R, l, r); }
	int erfen(int val) { return erfen(1, L, R, val); }
};

SEG seg(1, N);
//开了两个线段树, 下面线段树是用来维护suf数组的
SEG seg2(1, N);
int A[N], B[N], pre1[N], pre2[N]; //这里用的pre2数组,实际就是suf数组
//--------------------------------------------------------------------------------

signed main() {
	fileRead();
	kuaidu();
	T = 1;
	// cin >> T;
	while (T--) {
		cin >> n;
		int mmax = 0, sum = 0;
		rep(i, 1, n) {
			cin >> A[i];
			sum += A[i];
			cmax(mmax, A[i]);
		}
		seg.clear(1, n);
		seg2.clear(1, n);
		rep(i, 1, n) {
			B[i] = A[n - i + 1];
			pre1[i] = max(pre1[i - 1], A[i]);
			pre2[i] = max(A[n - i + 1], pre2[i - 1]);
			seg.add(i, i, pre1[i]);
			seg2.add(i, i, pre2[i]);
		}
		// cc(seg.qry(1, n).sum);
		cin >> m;
		rep(i, 1, m) {
			int a, b;
			cin >> a >> b;
			A[a] += b;
			cmax(mmax, A[a]);
			sum += b;
			int l, r;
			int val = seg.qry(a, a).mmin;
			if (A[a] > val) {
				l = a, r = seg.erfen(A[a]);
				if (l <= r)seg.add2(l, r, A[a]);
			}
			// cc(seg.qry(1, n).sum);
			B[n - a + 1] += b;
			// seg2.add(n - a + 1, n - a + 1, b);
			val = seg2.qry(n - a + 1, n - a + 1).mmin;
			if (B[n - a + 1] > val) {
				l = n - a + 1, r = seg2.erfen(B[n - a + 1]);
				if (l <= r) seg2.add2(l, r, B[n - a + 1]);
			}
			// cc(l, r);
			// cc(seg2.qry(1, n).sum);

			cc(seg.qry(1, n).sum + seg2.qry(1, n).sum - sum - n * mmax);
			// rep(j, 1, n) {
			// cc(seg2.qry(j, j).sum);
			// }
		}
		// cc(mmax);
	}
	return 0;
}

Problem D. 量子隧道穿梭

其实这个题就是一个暴力bfs题...

感觉大家都觉得暴力不靠谱的样子没有写bfs,但实际上一个简单的bfs就够了。定位是基础搜索题。

用一个bfs,然后同时对走过的点进行标记,这样就可以了。

顺便一提,其实不难发现,直达直径另一端节点这个操作我们最多只会执行一次,因为再执行一次其实就是回到原来的位置了。哪怕是你先执行一次,然后走几步,然后再执行一次,其实也相当于就是不执行,然后还是走那么几步。

所以如果k有值,那么我们就把到达终点的步数和到达终点对端的那个点的步数+1比较,取一个min就好了。

//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int dp[N];
bool vis[N];
int st, ed, shun, ni, k;
//--------------------------------------------------------------------------------
//struct or namespace:

//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n >> k >> st >> ed >> shun >> ni;
        st -= 1, ed -= 1;
        rep(i, 0, n - 1) dp[i] = INF; //dp数组用来记录到达第i个点的步数
        queue<PII> F;
        F.push({ st, 0 });
        while (!F.empty()) {
            auto [x, val] = F.front();
            F.pop();
            if (vis[x]) continue;
            vis[x] = 1;//vis数组用来打标记,防止走重复。
            dp[x] = val;
            F.push({ (x + shun) % n, val + 1 });
            F.push({ (x - ni + n) % n, val + 1 });
        }
        int ans = dp[ed];
        if (k > 0) cmin(ans, dp[(ed + n / 2) % n] + 1);
        if (ans >= INF / 2)ans = -1;
        cc(ans);


    }
    return 0;
}

Problem F. 树上操作

这个题的定位和A题其实是有点接近防AK的程度,只不过这个不太好骗分。

由于感觉大家对于这个题全部都在想骗分而没有正解,我就少扯一点(bushi

首先二分,求所有点深度的最大值的最小值。

然后考虑怎么处理,首先可以想到要将深度以下的求一个,挪动的时候直接挪动这个点。

然后直接暴力枚举每个点和他交换之后满不满足就好了。

判断满不满足的时候我们求一个向下深度的最大值就好了。

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
 
//--------------------------------------------------------------------------------
//struct or namespace:
namespace z {
    vector<PII> A[N];
    int son[N], dep[N], fa[N], dis[N];
    //重儿子,顶根,时间戳,子树最右端时间戳,时间戳会对应节点x
    int hea[N], up[N], dnf[N], dnff[N], seq[N];
 
    int len[N];
    int tot;
 
    void dfs(int x, int pa) {
        son[x] = 1, dep[x] = dep[pa] + 1, fa[x] = pa;
        int t = 0;
        for (auto [y, val]: A[x]) {
            if (y == pa) continue;
            dis[y] = dis[x] + val;
            dfs(y, x);
            cmax(len[x], len[y] + 1);
            son[x] += son[y];
            if (!t or son[t] < son[y]) t = y;
        }
        hea[x] = t;
    }
 
    void dfs2(int x, int pa, int ding) {
        dnf[x] = ++tot, up[x] = ding, seq[dnf[x]] = x;
        if (hea[x]) dfs2(hea[x], x, ding);
        for (auto [y, val]: A[x]) {
            if (y == pa || y == hea[x]) continue;
            dfs2(y, x, y);
        }
        dnff[x] = tot;
    }
 
    void clear(int n) {
        tot = 0;
        rep(i, 1, n) {
            A[i].clear();
            dis[i] = hea[i] = up[i] = dnf[i] = seq[i] = 0;
        }
    }
 
    void add(int x, int y, int c = 1) {
        A[x].push_back({y, c});
        A[y].push_back({x, c});
    }
 
    int lca(int x, int y) {
        while (up[x] != up[y]) {
            if (dep[up[x]] < dep[up[y]]) swap(x, y);
            x = fa[up[x]];
        }
        if (dep[x] > dep[y]) swap(x, y);
        return x;
    }
 
    //返回x向上k次的点
    int upup(int x, int k) {
        if (dep[x] <= k) return -1;
        int to = dep[x] - k;
        while (dep[up[x]] > to) x = fa[up[x]];
        return seq[dnf[x] - (dep[x] - to)];
    }
 
    int dist(int x, int y) { return dis[x] + dis[y] - dis[lca(x, y)] * 2; }
 
    void work(int rt = 1) { dfs(rt, 0), dfs2(rt, 0, rt); }
 
    //x的子树里有y就返回1
    bool isFa(int x, int y) { return (dnf[x] <= dnf[y] and dnff[x] > dnf[y]); }
};
 
//--------------------------------------------------------------------------------
 
bool check(int mid) {
    using namespace z;
    int t = 0;
    rep(i, 1, n) {
        if (dep[i] < mid) continue;
        if (len[i] == 0) continue;
        if (t == 0) t = i;
        if (isFa(t, i)) continue;
        t = lca(t, i);
    }
    if (t == 0) return 1;
    if (len[1] + 1 <= mid) return 1;
    rep(i, 1, n) {
        if (isFa(t, i) or isFa(i, t)) continue;
        if (dep[i] >= mid) continue;
        if (dep[t] + len[i] <= mid and dep[i] + len[t] <= mid) return 1;
    }
 
    return 0;
}
 
signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        z::clear(n);
        rep(i, 1, n-1) {
            int a, b;
            cin >> a >> b;
            z::add(a, b);
        }
        z::work();
        int l = 0, r = n + 1;
        while (l + 1 != r) {
            int mid = l + r >> 1;
            if (check(mid)) r = mid;
            else l = mid;
        }
        cc(r);
    }
    return 0;
}
 
/*
 
 
*/

Problem G. 最长序列

其实不难发现,由于我们可以随意的排序,所以可以直接全部从小到大的排序,然后去重就好了。

实际上我们也不需要做从小到大的排序,只需要对数组去重就好了,直接用一个set就好了。

不知道set的可以baidu一下。c++里一个可以去重的容器。

//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//struct or namespace:

//--------------------------------------------------------------------------------

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        set<int> S;
        rep(i, 1, n) {
            int a;
            cin >> a;
            S.insert(a);
        }
        cc(S.size());
    }
    return 0;
}

Problem I. 能源切割

貌似这个题的描述不是那么的清楚,赛时有被提问过题意的具体含义。

其实这个题就是一个背包,但是不想出一个比较裸的背包,所以就变了一点点的花样(没敢变太多,害怕没人过了)

我们可以使一个物品免费,但是这个物品左边的所有的物品我们的花费不能超过m,右边也一样。

回想一下正常的背包问题,01背包的式子是:

i是代表的前i个物品,j是容量。

我们让一个物品免费,就是直接加上他的v_i,然后左侧所有物品,花费不能超过m其实就是dp[i-1][m],右侧的物品呢?

我们反着求一个背包,得到dp2数组,那么dp2[i+1][m]就是代表的i后面的物品,花费不超过m的最大价值。

把这三个加起来就是我们选择i免费的贡献,取所有i的一个max就好了。

//--------------------------------------------------------------------------------
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------
//struct or namespace:

//--------------------------------------------------------------------------------

int pre[N][N], suf[N][N];

int val[N], cost[N];

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n >> m;
        rep(i, 1, n) {
            cin >> cost[i] >> val[i];
        }

        rep(i, 1, n) {
            rep(j, 1, m) {
                if (j >= cost[i]) {
                    pre[i][j] = max(pre[i][j], pre[i - 1][j - cost[i]] + val[i]);
                }
                pre[i][j] = max(pre[i][j], pre[i - 1][j]);
            }
        }

        rep2(i, n, 1) {
            rep(j, 1, m) {
                if (j >= cost[i]) {
                    suf[i][j] = max(suf[i][j], suf[i + 1][j - cost[i]] + val[i]);
                }
                suf[i][j] = max(suf[i][j], suf[i + 1][j]);
            }
        }

        int ans = 0;
        rep(i, 1, n) {
            ans = max(ans, val[i] + (pre[i - 1][m] + suf[i + 1][m]));
        }
        cc(ans);


    }
    return 0;
}

Problem K. 充能调节

这个题其实更多的是细节,有一些坑需要注意。经过大家反复的试错,其实最后过题率还是比较高的。

说几个点,首先是要记得开long long,还有就是m有可能是1,会导致一直乘下去而没有大于n的时候。

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N];
//--------------------------------------------------------------------------------
//struct or namespace:

//--------------------------------------------------------------------------------
int dfs(int b, int a) {
    if (b == 1) return 1;
    int ans = 0, mmax = INF;
    //l是次数 mmax是最小值 ans是记录的答案的次数
    int l = 1, bas = b;
    while (b <= a) {
        if (abs(b - a) < mmax) mmax = abs(b - a), ans = l;
        l++;
        b = b * bas;
    }
    if (abs(b - a) < mmax) mmax = abs(b - a), ans = l;
    return ans;
}

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        rep(i, 1, n) {
            int a, b;
            cin >> a >> b;
            cc(dfs(b, a));
        }
    }
    return 0;
}