2025牛客寒假算法基础集训营4


A. Tokitsukaze and Absolute Expectation

题意:ai[li,ri]独立等概率生成,求i=2n|aiai1|的期望。

可以单独求出每个位置和前面位置的的期望再相加。
那么问题变成了给你两个区间,求它们差的绝对值的期望。
如果两个区间不相交,假设li1<li,ri1<ri,那么我们如果固定ai1=li1,则有rili+1个值,且发现这些值是一个等差序列。然后将ai1每次加一继续观察生成的每个等差序列,发现每个等差序列的每个位置比上一个等差序列都相差1,那么就比上一个序列总共少了rili+1,发现这也是一个等差序列,项数为ri1li1+1
然后考虑重叠的情况,我们可以先把重叠的区间算出来,然后重叠区间的左边与其右边的贡献,它们是不相交的可以直接算。同理计算重叠区间右边与其左边的贡献,这两部分会有一段区间重叠,注意一下范围。然后考虑怎么算重叠的区间的贡献,打表发现是一个对称的矩阵,第i行除去前i1个,后面是首项为0公差为1的等差数列,那么变成了求i=1nn(n1的和,推理得n(n+1)(n1)3

点击查看代码
void solve() {
    int n;
    std::cin >> n;
    std::vector<int> l(n), r(n);
    for (int i = 0; i < n; ++ i) {
    	std::cin >> l[i] >> r[i];
    }

    auto calc = [&](int l1, int r1, int l2, int r2) -> Z {
        if (l1 > r1 || l2 > r2) {
            return 0;
        }

        Z n = r2 - l2 + 1, m = r1 - l1 + 1;
        Z a = n * (l2 - l1 + r2 - l1) / 2, an = a - n * (m - 1);
        Z res = m * (a + an) / 2;
        return res;
    };

    Z ans = 0;
    for (int i = 1; i < n; ++ i) {
        int x = std::max(l[i - 1], l[i]), y = std::min(r[i - 1], r[i]);
        Z sum = 0;
        if (x <= y) {
            int len = y - x + 1;
            sum += (Z)len * (len + 1) * (len - 1) / 3;
        }

        if (l[i - 1] <= l[i]) {
            sum += calc(l[i - 1], std::min(r[i - 1], x - 1), l[i], r[i]);
        } else {
            sum += calc(l[i], std::min(r[i], x - 1), l[i - 1], r[i - 1]);
        }

        if (r[i - 1] <= r[i]) {
            sum += calc(x, r[i - 1], std::max(l[i], y + 1), r[i]);
        } else {
            sum += calc(x, r[i], std::max(l[i - 1], y + 1), r[i - 1]);
        }

        ans += sum / ((Z)(r[i] - l[i] + 1) * (r[i - 1] - l[i - 1] + 1));
    }

    std::cout << ans << "\n";
}

B. Tokitsukaze and Balance String (easy) && C. Tokitsukaze and Balance String (hard)

题意:给你一个01串,有些地方是'?'表示没有填。一个01串的价值看它有多少位置满足取反后整个串10和01的个数相等。求所有可能的01串的价值之和。

这题正解是找规律分类讨论,赛时没多想写了个dp。
手玩一下发现,中间的位置不管怎么变都不会影响01串的平衡性。于是如果这个01串本来就是平衡的则贡献为n2。然后10的个数和01的个数差值的绝对值不超过1,因为你一个一个往后填01发现,两个01必然贡献一个10,两个10必然贡献一个01。赛时我也是根据这个性质进行的dp。然后发现如果s1==sn则10和01的个数相等,进行分类讨论就行。中间n2个产生的贡献是固定的。
讲讲我的dp解法,f[i][0/1][1/0/1],表示第i个位置填0/1并且10个数减01个数为1/0/1时的方案数,那么答案就是(f[n][0][0]+f[n][1][0])×(n2),然后如果s1!=0, 发现只要变化一定使得差值减一,则加上f[n][0][1]+f[n][1][1],如果sn!=1,则加上f[n][0][0]+f[n][1][0]sn类似的讨论,其实最后这个讨论就是正解的讨论,我也是写完了才发现dp了个寂寞。因为有负数下标,第三维都加个1变成0/1/2

点击查看代码
void solve() {
    int n;
    std::cin >> n;
    std::string s;
    std::cin >> s;

    std::vector f(n + 1, std::array<std::array<Z, 3>, 2>{});
    if (s[0] == '?') {
    	f[1][0][1] = f[1][1][1] = 1;
    } else if (s[0] == '0') {
    	f[1][0][1] = 1;
    } else {
    	f[1][1][1] = 1;
    }

    for (int i = 1; i < n; ++ i) {
    	if (s[i] != '0') {
    		f[i + 1][1][0] = f[i][1][0] + f[i][0][1];
    		f[i + 1][1][1] = f[i][1][1] + f[i][0][2];
    		f[i + 1][1][2] = f[i][1][2];
    	} 

    	if (s[i] != '1') {
    		f[i + 1][0][0] = f[i][0][0];
    		f[i + 1][0][1] = f[i][0][1] + f[i][1][0];
    		f[i + 1][0][2] = f[i][0][2] + f[i][1][1];
    	}
    }

    if (n == 1) {
    	std::cout << (f[n][0][1] + f[n][1][1]) << "\n";
    	return;
    }

    Z ans = (f[n][0][1] + f[n][1][1]) * (Z)(n - 2);
	if (s[0] != '0') {
		ans += f[n][0][2] + f[n][1][2];
	}

	if (s[0] != '1') {
		ans += f[n][0][0] + f[n][1][0];
	}

	if (s.back() != '0') {
		ans += f[n][0][0] + f[n][1][0];
	}

	if (s.back() != '1') {
		ans += f[n][0][2] + f[n][1][2];
	}

    std::cout << ans << "\n";
}

D. Tokitsukaze and Concatenate‌ Palindrome

题意:给你两个字符串a,b,你可以将它们都重新排列然后拼接起来。你还可以修改a里的元素,求让a+b是回文最小要修改几次。

因为可以重新排列,那么我们尽可能把相同的字符放到对应的位置。然后长串是可以放一些字符到另一边自己和自己匹配的,于是就是统计一下有多少个字符不能匹配,直接除二。因为两边不能匹配的字符数肯定是相等的。如果是奇数个,那么a+b长度肯定也是奇数,多出来的可以放中间,答案也是直接除二就行。

点击查看代码
void solve() {
    int n, m;
    std::cin >> n >> m;
    std::string s, t;
    std::cin >> s >> t;
    std::array<int, 26> cnt{};
    for (auto & c : s) {
    	cnt[c - 'a'] += 1;
    }

    for (auto & c : t) {
    	cnt[c - 'a'] -= 1;
    }

    int ans = 0;
    int tot = std::max(n, m) - (n + m + 1) / 2;
    for (int i = 0; i < 26; ++ i) {
    	if (cnt[i] < 0 && n <= m) {
    		cnt[i] = -cnt[i];
			int t = std::min(tot, cnt[i] / 2);
			cnt[i] -= t * 2;
			tot -= t;
    	} else if (cnt[i] > 0 && n >= m) {
			int t = std::min(tot, cnt[i] / 2);
			cnt[i] -= t * 2;
			tot -= t;    		
    	}
    }

    for (int i = 0; i < 26; ++ i) {
    	ans += std::abs(cnt[i]);
    }

    std::cout << ans / 2 << "\n";
}

E. Tokitsukaze and Dragon's Breath

题意:给你一个矩阵,可以选择一个位置得到这个位置所处主对角线和次对角线上所有元素的和。求最大值。

每个主对角线的i+j相等,每个次对角线的ij相等,存下来枚举取那个位置的对角线即可。

点击查看代码
void solve() {
    int n, m;
    std::cin >> n >> m;
    std::vector a(n, std::vector<int>(m));
    std::vector<i64> sum1((n + m) * 2), sum2((n + m) * 2);
    for (int i = 0; i < n; ++ i) {
    	for (int j = 0; j < m; ++ j) {
    		std::cin >> a[i][j];
    		sum1[i + j + n + m] += a[i][j];
    		sum2[i - j + n + m] += a[i][j];
    	}
    }

    i64 ans = 0;
    for (int i = 0; i < n; ++ i) {
    	for (int j = 0; j < m; ++ j) {
    		ans = std::max(ans, sum1[i + j + n + m] + sum2[i - j + n + m] - a[i][j]);
    	}
    }
    std::cout << ans << "\n";
}

F. Tokitsukaze and Kth Problem (easy)

题意:求(i<j),(ai+aj)%p的第1k大值。

做法一:
先把数组每个数对p取模,然后排序。
考虑二分求第k值,每次看(ai+aj)%p>=mid的个数是不是大于等于k,每个点对分为两种情况,一种是ai+aj<p,ai+aj>=mid,一种是ai+aj>p,ai+ajp>=mid。check可以双指针来写,先不管取模的情况,直接双指针求所有>=mid的数量,然后减去所有和大于等于p的点对的数量,然后加上大于等于mid+p的数量,发现正好把ai+aj>=p,ai+ajp<mid的情况给减去了。
求出第k大数后,可以暴力的求出前k+1的所有值,因为第k大值可能有很多,但第k+1大的数量必然小于k,所以暴力不会超时。具体也是双指针维护,分两种情况求。
最后一直加第k大直到答案够k个。
参考了题解代码。

点击查看代码
void solve() {
    int n, p, m;
    std::cin >> n >> p >> m;
    std::vector<int> a(n);
    for (int i = 0; i < n; ++ i) {
    	std::cin >> a[i];
    	a[i] %= p;
    }

    std::sort(a.begin(), a.end());

    auto get = [&](int x) -> i64 {
    	i64 res = 0;
    	for (int i = 0, j = n - 1; i < n; ++ i) {
    		while (j >= 0 && a[i] + a[j] >= x) {
    			-- j;
    		}

    		res += n - 1 - std::max(i, j);
    	}

    	return res;
    };

    i64 getp = get(p);
    auto check = [&](int x) -> i64 {
    	return get(x) - getp + get(x + p);
    };

    int l = 0, r = p - 1;
    while (l < r) {
    	int mid = l + r + 1 >> 1;
    	if (check(mid) >= m) {
    		l = mid;
    	} else {
    		r = mid - 1;
    	}
    }

    if (check(l) < m) {
    	l = -1;
    }

    std::vector<int> ans;
    for (int i = 0, j = n - 1, k = n - 1; i < n; ++ i) {
    	while (j >= 0 && a[i] + a[j] > l) {
    		-- j;
    	}

    	while (k >= 0 && a[i] + a[k] > l + p) {
    		-- k;
    	}

    	for (int x = std::max(i, j) + 1; x < n && a[i] + a[x] < p; ++ x) {
    		ans.push_back(a[i] + a[x]);
    	}

    	for (int x = std::max(i, k) + 1; x < n; ++ x) {
    		ans.push_back((a[i] + a[x]) % p);
    	}
    }

    while (ans.size() < m) {
    	ans.push_back(l);
    }

    std::sort(ans.begin(), ans.end(), std::greater<int>());
    for (int i = 0; i < m; ++ i) {
    	std::cout << ans[i] << " \n"[i == m - 1];
    }
}

做法2:
一样的分成两种情况考虑,ai找到最大的p1i,使得ap1i+ai<p,那么这个位置一直往左走和ai得出的数是不断变小的。再搞一个p2i=n,因为ai+aj<2p,则ai+an如果超过了p那么一定是第二中情况里最大的。那么每个数的两种情况的最大值都放到优先队列里,每次取出最大的,然后看是属于哪一段的值,把指针往前移任何把和ai的和加到优先队列里就行了。

点击查看代码
void solve() {
    int n, P, m;
    std::cin >> n >> P >> m;
    std::vector<int> a(n);
    for (int i = 0; i < n; ++ i) {
    	std::cin >> a[i];
    	a[i] %= P;
    }

    std::sort(a.begin(), a.end());

    std::vector<int> p(n), p1(n), p2(n);
    std::priority_queue<std::pair<int, int>> heap;
    for (int i = 0; i + 1 < n; ++ i) {
        int j = std::lower_bound(a.begin(), a.end(), P - a[i]) - a.begin() - 1;
        p[i] = p1[i] = std::max(j, i);
        if (j > i) {
            heap.push({(a[i] + a[j]) % P, i});
        }

        p2[i] = n - 1;
        if (p2[i] > p[i]) {
            heap.push({(a[i] + a[n - 1]) % P, i});
        }
    }

    std::vector<int> ans;
    while ((int)ans.size() < m && heap.size()) {
        auto [x, i] = heap.top(); heap.pop();
        ans.push_back(x);
        if (p1[i] > i && (a[i] + a[p1[i]]) % P == x) {
            -- p1[i];
            if (p1[i] > i) {
                heap.push({(a[i] + a[p1[i]]) % P, i});
            }
        } else {
            -- p2[i];
            if (p2[i] > p[i]) {
                heap.push({(a[i] + a[p2[i]]) % P, i});
            }
        }
    }

    while (ans.size() < m) {
        ans.push_back(-1);
    }

    for (int i = 0; i < m; ++ i) {
        std::cout << ans[i] << " \n"[i == m - 1];
    }
}

G. Tokitsukaze and Kth Problem (hard)

待补


H. Tokitsukaze and Necklace

题意:你有一些'a', 'b', 'c'。这三个字母可以有27种排列,每种排列都有自己的贡献,你要用它们构造一个序列,序列首尾相连,其值等于每三个相邻字母的值。求最大值及方案。

赛时没看这题,dp思路还是很简单的。f[i][j][k][l][x][y]表示第i个为xi1个为y,总共用了j个'a',k个'b',l个'c'时的最大价值,转移就是枚举前面选什么就行,主要是代码长。然后发现知道j,k的值也就知道了l的值,可以省略一维变成f[i][j][k][x][y]
记录方案也很麻烦,但dp记方案的套路是固定的,记录每个状态是由哪个状态过来的,从后往前推出来就行。因为有环,可以枚举前两位放什么来破环。思路倒不是很难,具体看代码。

点击查看代码
const int N = 3;

int val[N][N][N];
int cnt[N];
i64 f[151][151][151][3][3];
std::array<int, 4> pre[151][151][151][3][3] = {};

void solve() {
    int n, m;
    std::cin >> n >> m;
    memset(val, 0, sizeof val);
    memset(cnt, 0, sizeof cnt);
    std::string s;
    std::cin >> s;
    for (auto & c : s) {
    	++ cnt[c - 'a'];
    }

    for (int i = 0; i < m; ++ i) {
    	std::string s;
    	int v;
    	std::cin >> s >> v;
    	val[s[0] - 'a'][s[1] - 'a'][s[2] - 'a'] = v;
    }

    std::string ans = s;
    const i64 inf = 1e18;
    i64 max_val = -inf;

    auto work = [&](int a, int b) -> void {
    	std::array<int, 3> tmp{};
    	tmp[a] += 1;
    	tmp[b] += 1;
    	for (int i = 0; i < 3; ++ i) {
    		if (tmp[i] > cnt[i]) {
    			return;
    		}
    	}

    	for (int i = 1; i <= n; ++ i) {
    		for (int j = 0; j <= cnt[0] && j <= i; ++ j) {
    			for (int k = 0; k <= cnt[1] && j + k <= i; ++ k) {
    				if (i - j - k <= n - cnt[0] - cnt[1]) {
    					for (int x = 0; x < 3; ++ x) {
    						for (int y = 0; y < 3; ++ y) {
    							f[i][j][k][x][y] = -inf;
    						}
    					}
    				}
    			}
    		}
    	}

    	auto calc = [&](int i, int a, int b, int c, int d, int x, int y, int z) -> void {
    		if (c < 0 || d < 0) {
    			return;
    		}

    		if (a == c && b == d && i - a - b <= 0) {
    			return;
    		}

    		if (f[i - 1][c][d][y][z] + val[z][y][x] <= f[i][a][b][x][y]) {
    			return;
    		}

			f[i][a][b][x][y] = f[i - 1][c][d][y][z] + val[z][y][x];
			pre[i][a][b][x][y] = {c, d, y, z};
    	};

    	f[2][tmp[0]][tmp[1]][b][a] = 0;
    	for (int i = 3; i <= n; ++ i) {
    		for (int j = 0; j <= cnt[0] && j <= i; ++ j) {
    			for (int k = 0; k <= cnt[1] && j + k <= i; ++ k) {
    				if (i - j - k > n - cnt[0] - cnt[1]) {
    					continue;
    				}

    				for (int x = 0; x < 3; ++ x) {
    					for (int y = 0; y < 3; ++ y) {
    						calc(i, j, k, j - 1, k, 0, x, y);
    						calc(i, j, k, j, k - 1, 1, x, y);
    						calc(i, j, k, j, k, 2, x, y);
    					}
    				}
    			}
    		}
    	}

    	i64 max = -inf;
    	std::array<int, 4> end{};
    	for (int x = 0; x < 3; ++ x) {
    		for (int y = 0; y < 3; ++ y) {
    			i64 v = f[n][cnt[0]][cnt[1]][x][y] + val[y][x][a] + val[x][a][b];
    			if (v > max) {
    				max = v;
    				end = {cnt[0], cnt[1], x, y};
    			}
    		}
    	}

    	if (max > max_val) {
    		max_val = max;
    		ans.clear();
    		for (int i = n; i > 2; -- i) {
    			// std::cerr << end[0] << " " << end[1] << " " << end[2] << " " << end[3] << "\n";
    			ans += char(end[2] + 'a');
    			end = pre[i][end[0]][end[1]][end[2]][end[3]];
    		}

    		ans += char(b + 'a');
    		ans += char(a + 'a');
    	}
    };

    for (int a = 0; a < 3; ++ a) {
    	for (int b = 0; b < 3; ++ b) {
    		work(a, b);
    	}
    }

	std::reverse(ans.begin(), ans.end());

    std::cout << max_val << "\n";
    std::cout << ans << "\n";
}

I. Tokitsukaze and Pajama Party

题意:求每个"uwawauwa"前面有多少"u"。

模拟统计即可。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
    std::string s;
    std::cin >> s;
    std::string t = "uwawauwa";
    int len = t.size();
    i64 ans = 0, cnt = 0;
    for (int i = 0; i + len - 1 < n; ++ i) {
    	if (s.substr(i, len) == t) {
    		ans += cnt;
    	}

    	if (i > 0 && s[i - 1] == 'u') {
    		++ cnt;
    	}
    }

    std::cout << ans << "\n";
}

J. Tokitsukaze and Recall

题意:有若干个联通块,你可以选k个点,可以随时传送到这k个点的任意一个。你要占领尽可能多的点。并给出移动方案。要求字典序最小。

首先求出所有连通块。设有cnt个。
要占领尽可能的点,那么我们肯定先联通块大小从大到小的给每个联通块一个传送点,如果大小相同,则最小点更小的优先。然后可以搜索方案,我们给每个联通块最小的点一个传送点,然后我们用优先级队列来搜索使得我们每次可以走编号最小的点。如果还有多余的传送点,并且当前到的点不是可以到的剩余点里的最小的,那么可以给这个剩余点里最小的点一个传送点。模拟即可。

点击查看代码
struct DSU {
	std::vector<int> fa, cnt;
	DSU(int _n) {
		init(_n);
	}

	void init(int _n) {
		fa.assign(_n + 1, 0);
		cnt.assign(_n + 1, 1);
		std::iota(fa.begin(), fa.end(), 0);
	}

	int find(int x) {
		return x == fa[x] ? x : fa[x] = find(fa[x]);
	}

	bool merge(int x, int y) {
		x = find(x), y = find(y);
		if (x == y) {
			return false;
		}

		fa[y] = x;
		cnt[x] += cnt[y];
		return true;
	}

	bool same(int x, int y) {
		return find(x) == find(y);
	}

	int size(int x) {
		return cnt[find(x)];
	}
};

void solve() {
    int n, m, k;
    std::cin >> n >> m >> k;
    std::vector<std::vector<int>> adj(n);
    DSU d(n);
    for (int i = 0; i < m; ++ i) {
    	int u, v;
    	std::cin >> u >> v;
    	-- u, -- v;
    	if (u > v) {
    		std::swap(u, v);
    	}
    	adj[u].push_back(v);
    	adj[v].push_back(u);
    	d.merge(u, v);
    }

    std::vector<std::vector<int>> blocks(n);
    int cnt = 0;
    for (int i = 0; i < n; ++ i) {
    	blocks[d.find(i)].push_back(i);
    	cnt += d.find(i) == i;
    }

    std::sort(blocks.begin(), blocks.end(), [&](std::vector<int> & a, std::vector<int> & b) {
    	if (a.size() && b.size() && a.size() == b.size()) {
    		return a[0] < b[0];
    	}

    	return a.size() > b.size();
    });


    int t = std::min(k, cnt);
    k = std::max(0, k - cnt);
	std::priority_queue<int, std::vector<int>, std::greater<int> > heap;
    std::vector<int> st(n);
    std::set<int> s;
    for (int i = 0; i < t; ++ i) {
		for (auto & x : blocks[i]) {
			s.insert(x);
		}
		heap.push(blocks[i][0]);
		st[blocks[i][0]] = 1;
    }

    std::vector<int> ans;
	while (heap.size()) {
		if (heap.top() != *s.begin() && k > 0) {
			-- k;
			heap.push(*s.begin());
			st[*s.begin()] = 1;
		}
		int u = heap.top(); heap.pop();
		s.erase(u);
		ans.push_back(u);
		for (auto & v : adj[u]) {
			if (!st[v]) {
				st[v] = 1;
				heap.push(v);
			}
		}
	}

    std::cout << ans.size() << "\n";
    for (auto & x : ans) {
    	std::cout << x + 1 << " \n"[x == ans.back()];
    }
}

K. Tokitsukaze and Shawarma

签到题。

点击查看代码
void solve() {
    int x, y, z, a, b, c;
    std::cin >> x >> y >> z >> a >> b >> c;
    std::cout << std::max({x * a, y * b, z * c}) << "\n";
}

L. Tokitsukaze and XOR-Triangle

题意:多次询问求一个区间内两两异或和的和。

按位计算,那么就变成了在一个01串里询问一个区间里有多少01和10。
可以前缀和求出来每个位置和它前面的数产生了多少贡献。但求[l,r]时,发现sumrsuml1这个贡献里[l,r]里数算上了[1,l1]这些数产生的贡献,考虑减去,设[1,l1]cnta0个0,cnta1个1,[l,r]里有cntb0个0,cntb1个1。那么需要减去的贡献为cnta0×cntb1+cnta1×cntb0

点击查看代码
void solve() {
    int n, q;
    std::cin >> n >> q;
    std::vector<i64> a(n + 1), b(n + 1);
    for (int i = 1; i <= n; ++ i) {
    	std::cin >> a[i];
    }

    for (int i = 1; i <= n; ++ i) {
    	std::cin >> b[i];
    }

    std::vector suma(n + 1, std::array<i64, 30>{});
    std::vector sumb(n + 1, std::array<i64, 30>{});
    for (int i = 1; i <= n; ++ i) {
    	suma[i] = suma[i - 1];
    	sumb[i] = sumb[i - 1];
    	for (int j = 0; j < 30; ++ j) {
    		suma[i][j] += a[i] >> j & 1;
    		sumb[i][j] += b[i] >> j & 1;
    	}
    }

   	std::vector sum(n + 1, std::array<i64, 30>{});
    for (int i = 1; i <= n; ++ i) {
    	sum[i] = sum[i - 1];
    	for (int j = 0; j < 30; ++ j) {
    		if (b[i] >> j & 1) {
    			sum[i][j] += i - suma[i][j];
    		} else {
    			sum[i][j] += suma[i][j];
    		}
    	}
    }

    for (int i = 0; i < q; ++ i) {
    	int l, r;
    	std::cin >> l >> r;
    	Z ans = 0;
    	for (int i = 0; i < 30; ++ i) {
			i64 cnta1 = suma[l - 1][i], cnta0 = l - 1 - cnta1;
			i64 cntb1 = sumb[r][i] - sumb[l - 1][i], cntb0 = r - l + 1 - cntb1;
			ans += (Z)(1ll << i) * (sum[r][i] - sum[l - 1][i] - cnta1 * cntb0 - cnta0 * cntb1);
		}
		std::cout << ans << "\n";
    }
}
posted @   maburb  阅读(107)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示