题解 | #欢迎来到算法世界#
第十届CUIT - ACM程序设计竞赛 - 题解
本题解为官方题解,题号按照现场赛写的。
注意同步赛与现场赛的题号、顺序不一样。
[TOC]
标签预览
怎么走啊(拓扑
缩边
)
我来组成头部(构造)
欢迎来到算法世界(签到)
娇の礼帽(思维
策略)
区间异或(前缀异或
排序)
不存在的子串(二维数点:树状数组)
三人成行(排序)
若你来到南京 是否想起我的梧桐(离散化
线段树优化
)
参加算法竞赛(二分优化
)
我上面有人儿(并查集)
这是回文吗(贪心
哈希)
更待西湖彻底干 此间应有再生缘 (思维
策略)
预期难度定位: 简单 ,中档
,较难
。
现场赛排行榜效果: 简单 ,过度
,中档
,中难
,较难
。
下文按照现场赛排行榜效果排序。
思路讲解:
欢迎来到算法世界
输出即可,注意转义。
三人成行
发现排序后直接三人一组即可使得差异最小。
简易理解:
一个组内差异是最大减最小,我们将其描述为一条线段,就有(意会一下)
更待西湖彻底干 此间应有再生缘
答案 次。
首先因为一个人不可能抱自己和伴侣,所以次数只可能为 ;
其次除开小林的 人又各不相同,那只能是
;
我们考虑某人抱了 次,那么意味着他抱了除开他和伴侣之外所有人,所以被抱的人已经至少算一次了, 那么
次的人只能是
次的人的伴侣。
把 对情侣画出来(此时并不知道小林小雅是哪一对),先把
次的写最边上。
又从剩余的最大值 来考虑,同理可得他与
次的人为情侣。
以此类推,发现最后一对情侣次数相同,都是 次。
又因为题设除开小林其他人都不一样,那小林只能是最后一对之一,所以其伴侣小雅自然也是 。
娇の礼帽
答案 。
每个人帽子只有黑白两种可能,我们令白色为 ,黑色为
,令第
个人帽子颜色是
。
我们策划回答顺序为 倒着来,第
个人最先回答
的颜色异或值,简单记为
。
那么第 个人可以看到前
人,得到前面的异或值
,由于他听到了
号曝出的
,所以第
就知道自己头上就是
。
同理,第 看得到
,又已经听到了
和
, 可以得到
。
其他人以此类推,所以这种方案只有第 个人
概率答对,其他
人必对,则期望为
。
我上面有人儿
并查集模板,出此题是教学意义。
使用题面的伪代码可以查询到老大,只需要开个数组记录老大名下的人数,遍历每个人,查询其老大,然后给老大的计数加一。
再重新遍历每个人,查询其老大,然后输出老大对应的名下人数即可。
另外,并查集可以用 循环写法,就不需要担心递归深度了。
还有“路径压缩”技巧,即在找老大的过程中把路径上所有人的“领导”都直接修改成老大,那么下次查询的路径长度就只有 了,这样更快。
我来组成头部
构造题答案不唯一,题解做法是 。
参加算法竞赛
观察发现,选出来的序列可以理解成两个上升子序列(简称 )拼起来的。
可以做一遍1到n的最长上升子序列 ,再做一遍
到
的最长下降子序列
;
然后枚举 作为拼接点,用
更新答案即可。
注意到数据规模较大,所以求 不能用
做法,需要优化。
思考下标、 长度、
最后一个数字的值三者的两两映射关系。
我们想要的是 下标
长度,转移时是找其
末尾值小于自己 的 最大
长度;
第一种,也比较自然的优化是:遍历下标过程中维护映射 末尾值
长度,用树状数组做前缀最值查询。
第二种,也是 写法的优化是:遍历下标过程中维护映射
长度
最小末尾值,这个数组显然有单调性,每次可以二分找到
最多能接在多长的上升子序列末尾,即求出了以
结尾的
长度。
这是回文吗
第一步贪心的想,先双指针从 的头部和
的尾部开始尽量选一个最长的前后缀满足两个拼起来是回文;
第二步假设两个指针最后停在 和
位置,现在再求出
中以
为起始的最长回文串,求出以
为结尾的最长回文串,选两者更长的保留下来。
则答案就是双指针跑过的长度 第二步保留的更长回文串长度。
其中第二步的实现是多样的;
第一种,可以用马拉车做预处理
第二种,也是 的做法,可以预处理前后缀哈希,在枚举以i开始的所有字符串长度
的过程中,用哈希对比字符串
和
即可。
第三种,回文自动机....(高手自便)
另外,第一步贪心的简易证明:
反证法,我们假设最优解在没有选满前后缀的地方,我们看如果强行选满会不会让答案更小;
如图,最优解如果在不选满的地方,再强行选满,这个过程一定满足 ,则可以把
补到
的地方,此时答案和选满的答案是一样的,所以选满并不会使得解变得比最优解小,即贪心选满前后缀可以得到最优解之一。
区间异或
首先考虑 的情况,小于
,即求异或值为
的区间个数。
先做前缀异或,显然前缀异或值相同的两个位置所成区间异或值为 ;
于是只需要对前缀异或相同的值计数,两两配对的组合数为 ,累加组合数则为答案。
现在考虑 的情况,其实只需要把所有数字右移
位,就转换成了区间异或为
的问题。
但若对每个 单独处理前缀异或再用
计数是
的复杂度,不足以通过本题,需要考虑优化。
第一种,很自然的做法是手写写哈希表计数,写得好就能过本题。
第二种,用字典树储存前缀异或的所有值,遍历过程中累加答案,深度对应 ,子树叶子个数对应
,做到
预处理,
询问。
第三种,也是 的做法,只需要把前缀异或排序,用双指针来实现
的相同数字计数;在右移
之后并不会影响有序性,所以可以直接跑
的双指针。因为只需要排序一次,复杂度
,对于每种询问需要
计数,复杂度
,所以综合复杂度
,且很好写。
怎么走啊
关键点:所有点连通、边数不超过点数 、起点终点固定、没有负权边。
于是可以理解为在一棵树上添加了不超过 条边,起点终点又是固定的,其实可以想到核心的点不会太多。
其 的做法是:
第一步,先拓扑排序,把除开 和
之外的所有叶子一层层删掉直到删不掉为止,因为这些删掉的点是完全不可能成为答案的点。(这一步基于起点终点固定、没有负权边);
第二步,将除开 和
之外度数为
的点收缩掉(即把一条长链收缩为单独一条边),用一条新的双属性边来描述这条路。(双属性是指,原本一条边只有一个正向的权值,但如果把一条链全部收缩,则需要分别记录正向权值和反向权值)
可以想到,这样做之后剩下的点数只会在 左右,因为最开始保证了连通,所以边数也只有
左右。
那么对于每次询问,只需要在“缩略图”中直接跑 就可以了。预处理复杂度线性,询问一次
大约
,对于
次询问已经足够了。
另外还有选手使用了十分巧妙的二分做法:
对于一条路线,令反边权值和为 ,正边权值和为
,则其真实路线长度为
,几何意义为一条直线。
众多的路线可以理解为第一象限内的众多直线。
而对于每次询问,也就是确定k的情况下,从众多路线中选择最短路的行为,可以理解为在所有直线中选择最小的函数值。
那么当k从小到大变化起来,会发现其实我们选择的就是众多直线在第一象限内的下凸包表面(或许说是凹包更合理)。
由于图的边数限制,这个凸包表面的线段数量不会很多;
显然k越大所截取的凸包面的斜率越小,于是可以维护一个当前斜率,在确定一个凸包面线段左端点后,通过二分来求右端点, 方式是跑一遍最短路再看斜率大小。
每求出一段线段就可以把范围内的 直接计算完。于是这样做预处理的复杂度是
,询问是
的。
另外还有选手使用了十分巧妙的虚树做法:
但是我不会。
不存在的子串
模型可以抽象为HH的项链,不会这个的先去洛谷搜搜做。
给 的主串,
次询问求区间内最短的未出现子串。首先答案不会超过
,因为长度为
的串有
种,
的主串不可能包含完。
所以可以考虑枚举答案 ,检查区间内长度为k的串是否全都出现过了,即区间内是否有
种不同的串。
考虑一种特别的哈希,即把字符串按照 进制唯一映射为一个数字,那么长度为k的所有串其实对应了
的数字区间。
我们把字符串的每一个位置开始长度为 的子串都映射为数字保存在新的数组中,那么问题就变成了:求四次在区间内不同的数字种类数是否为
个。
模型建立完毕,从数据规模判断,莫队并不足以通过本题。
所以可以先离线所有询问,然后遍历数列的过程中用树状数组维护每个数字最后一次出现的位置 ,处理到第
位的同时,处理所有右端点为
的询问,即查询此刻树状数组中
的和,直观意义是最后一次出现的数字个数,同时也是区间内不同数字的个数。
以上。当然也可以写主席树维护 的计数(代表上一个
出现的位置),直接在线查询对于区间
,满足
的点的个数。
若你来到南京 是否想起我的梧桐
分别是树高,存活价值,砍伐代价。
由题意阳光机制可以分析,最终活下来的树一定形成了高度“先上升再下降”的趋势。
于是考虑设计 表示保证第
颗树存活的情况下,前缀树高不下降子序列能产生的最大价值。
其暴力转移方式为,枚举 ,选一棵不高于自己树j作为上一颗必须存活的树,那么j~i的所有低于j的树都会因为缺乏阳光而自然枯萎,而不低于j的树都需要砍伐掉且消耗对应代价,以此保证j是上一颗树。
于是有转移方程 。
这样的暴力显然 不行,考虑优化。
如果考虑 的情况下维护对映射 树高
最后一棵树高为h的所有决策中能产生的最大价值 建立线段树求区间最值;
那么转移方程中 这一步就变成了求线段树中键小于等于
的最大值。
但不对,这没有体现出砍伐的代价。
思考对于第 颗树什么时候会被砍,只有在计算大于
的
状态,且选中了一颗高度小于等于自己的树作为“前驱”时,自己必然被砍,且造成负贡献。
所以只需要在处理完第 个
值后,先在线段树更新
的
值为
,再**把线段树内
都减去代价
**即可。
于是做到了 复杂度求出
,代表了以
结尾的前缀高度不下降最大价值;
同理再倒着求一个 ,代表以
起始的后缀高度不上升最大价值;
最后枚举第 位作为峰点,用
更新答案即可。
另外,树高一开始就离散化了更方便处理。
标程代码:
欢迎来到算法世界
#include <bits/stdc++.h>
using namespace std;
signed main()
{
cout << "printf(\"新华三杯 - CUIT_ACM竞赛开始啦\\n\");";
return 0;
}
三人成行
#include <bits/stdc++.h>
using namespace std;
int a[4000];
int main()
{
int n; cin>>n;
n *= 3;
for(int i=1;i<=n;i++)
cin>>a[i];
sort(a+1, a+1+n);
int ans=0;
for(int i=1;i<=n;i+=3)
ans += a[i+2]-a[i];
cout << ans <<"\n";
}
更待西湖彻底干 此间应有再生缘
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n; cin>>n;
cout << n-1;
return 0;
}
娇の礼帽
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n; cin>>n;
cout << n-0.5;
return 0;
}
我上面有人儿
#include <bits/stdc++.h>
using namespace std;
int A[1010], siz[1010];
int find(int u){
if(A[u]==u) return u;
return find(A[u]);
// 写return A[u]=find(A[u])就是"路径压缩",效率更高
}
int main(){
int n; cin>>n;
for(int i=1;i<=n;i++)
cin>>A[i];
for(int i=1;i<=n;i++)
siz[find(i)]++;
for(int i=1;i<=n;i++)
cout<<siz[find(i)]<<' ';
cout<<"\n";
}
我来组成头部
#include <bits/stdc++.h>
using namespace std;
int main()
{
int t; cin>>t;
while (t--)
{
int n; cin>>n;
printf("%d %d %d %d %d\n", 1, n, n-1, n-2, 2);
}
return 0;
}
参加算法竞赛
#include<bits/stdc++.h>
int main()
{
int n;
std::cin>>n;
std::vector<int>a(n+1);
for(int i = 1;i<=n;i++) {
std::cin>>a[i];
}
std::vector<int>lis1(n+5),lis2 = lis1;
std::vector<int>dp;
for(int i = 1;i<=n;i++) {
if(dp.empty()) {
dp.push_back(a[i]);
} else {
auto it = std::lower_bound(dp.begin(),dp.end(),a[i]);
if(it == dp.end()) {
dp.push_back(a[i]);
} else {
*it = a[i];
}
}
lis1[i] = dp.size();
}
dp.clear();
for(int i = n ; i >= 1 ; i--) {
if(dp.empty()) {
dp.push_back({a[i]});
} else {
auto it = std::lower_bound(dp.begin(),dp.end(),a[i],std::greater<int>());
if(it == dp.end()) {
dp.push_back(a[i]);
} else {
*it = a[i];
}
}
lis2[i] = dp.size();
}
int ans = 0;
for(int i = 0;i<=n;i++) {
ans = std::max(ans,lis1[i] + lis2[i+1]);
}
std::cout<<ans<<std::endl;
return 0;
}
这是回文吗
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
struct HASH{
static const int base = 139;
static const int N = 1e6+10; // 记得改长度
static const ll MOD = 1e9+7;
static ll flag, pow[N];
vector<ll> v;
HASH()=default;
HASH(const string &s){
if(flag==0) flag=1, init_fac();
hash(s);
}
void init_fac(){
pow[0]=1;
for(int i=1;i<N;i++)
pow[i] = pow[i-1]*base%MOD;
}
// 保证给入的字符串是下标从1开始
void hash(const string &s){
v.resize(s.size(),0);
for(int i=1;i<s.size();i++)
v[i] = (v[i-1]*base+s[i])%MOD;
}
// 区间哈希
ll hash(int l, int r){
return (v[r] - v[l-1]*pow[r-l+1]%MOD + MOD)%MOD;
}
};
ll HASH::flag, HASH::pow[N];
// 记得改长度N 记得改长度 记得改长度 记得改长度 记得改长度
signed main()
{
cin.tie(0), cout.tie(0), ios::sync_with_stdio(false);
int n; cin>>n;
string a,b;
cin>>a>>b;
reverse(b.begin(),b.end());
a = " " + a;
b = " " + b;
if(a == b) {
cout<<2*n<<endl;
return 0;
}
int l = 1;
while(l<=n && a[l] == b[l]) l++;
int ans = (l-1) * 2;
auto work = [&](const string& str)->int {
int m = str.size();
string x = str;
reverse(x.begin(),x.end());
HASH s(' '+str),t(' '+x);
int maxp = 0;
for(int i = 1;i<=m;i++) {
if(s.hash(1,i) == t.hash(m-i+1,m)) {
maxp = i;
}
}
return maxp;
};
a = a.substr(l);
b = b.substr(l);
ans += max(work(a),work(b));
cout<<ans<<endl;
return 0;
}
区间异或
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int main(){
int n,q; cin>>n>>q;
vector<int> a(n+1);
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++) a[i]^=a[i-1];
sort(a.begin()+1, a.end());
vector<ll> ans(31);
for(int k=0;k<=30;k++){
int p=0;
for(int i=1;i<=n;i++){
if(a[i]!=a[p]) p=i;
else ans[k]+=i-p;
}
for(int i=1;i<=n;i++) a[i]>>=1;
}
while(q--){
int k; cin>>k;
cout << ans[k] << "\n";
}
}
怎么走啊
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 3, INF = 0x3f3f3f3f;
struct node
{
int v;
ll w, w2;
};
vector<vector<node>> top(vector<vector<node>> &g, int n)
{
vector<int> deg(n + 1);
for (int i = 2; i < n; ++i)
deg[i] = (int)g[i].size();
deg[1] = deg[n] = INF;
queue<int> q;
for (int i = 2; i < n; i++)
if (deg[i] == 1)
q.push(i);
vector<int> vis(n + 1);
while (!q.empty())
{
int u = q.front();
q.pop();
vis[u]=1;
for (auto &[v, w1, w2] : g[u])
if ((--deg[v]) == 1)
q.push(v);
}
vector<vector<node>> g1(n + 1);
for (int u = 1; u <= n; u++)
if (!vis[u])
for (auto &[v, w1, w2] : g[u])
if (!vis[v])
g1[u].push_back({v, w1, w2});
return g1;
}
int id[N], tot;
vector<vector<node>> sim(vector<vector<node>> &g, int n)
{
vector<int> vis2(n + 1);
vector<vector<node>> g1(n + 1);
function<void(int, int, int, ll, ll)> dfs1 = [&](int u, int f, int lst, ll ws1, ll ws2)
{
if (id[u])
{
if (lst)
{
g1[id[lst]].push_back({id[u], ws1, ws2});
g1[id[u]].push_back({id[lst], ws2, ws1});
}
lst = u, ws1 = 0, ws2 = 0;
}
if (vis2[u])
return;
vis2[u] = 1;
for (auto &[v, w1, w2] : g[u])
if (v != f)
dfs1(v, u, lst, ws1 + w1, ws2 + w2);
};
dfs1(1, 0, 0, 0, 0);
return g1;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m;
vector<vector<node>> g(n + 1);
for (int i = 0; i < m; i++)
{
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w, 0});
g[v].push_back({u, 0, w});
}
g = top(g, n);
for (int i = 1; i <= n; i++)
if (i == 1 || i == n || g[i].size() > 2)
id[i] = ++tot;
g = sim(g, n);
int q; cin >> q;
while (q--)
{
ll k; cin >> k;
struct w_node
{
int v;
ll w;
bool operator<(const w_node &o) const { return w > o.w; }
};
priority_queue<w_node> pq;
pq.push({1, 0});
vector<int> vis3(tot + 1);
vector<ll> dis(tot + 1, 1e18);
dis[1] = 0;
while (!pq.empty())
{
int u = pq.top().v; pq.pop();
if (vis3[u])
continue;
if (u==id[n])
break;
vis3[u] = 1;
for (auto &[v, w1, w2] : g[u])
if (dis[u] + w1 + w2 * k < dis[v])
{
dis[v] = dis[u] + w1 + w2 * k;
pq.push({v, dis[v]});
}
}
cout << dis[id[n]] << "\n";
}
return 0;
}
不存在的子串
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
struct Fenwick {
vector<int>tr;
int limit;
Fenwick() {}
Fenwick(int n) {
limit = n;
tr.assign(n+1,0);
}
void init(int n) {
limit = n;
tr.assign(n+1,0);
}
int lowbit(int x) {
return x & (-x);
}
void add(int p,int x) {
while(p <= limit) {
tr[p] += x;
p += lowbit(p);
}
}
int query(int p) {
int ans = 0;
while(p) {
ans += tr[p];
p -= lowbit(p);
}
return ans;
}
};
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int n; cin>>n;
string str; cin>>str;
str = " " + str;
int q; cin>>q;
vector<vector<array<int,2>>>seg(n+1);
for(int i = 0;i<q;i++) {
int l,r; cin>>l>>r;
seg[r].push_back({l,i});
}
int base = 1;
vector<int>ans(q,5);
for(int len = 1;len <= 4;len++) {
base *= 26;
Fenwick bit(N);
vector<int>vis(base*2,0);
for(int i = 1;i<=n;i++) {
if(i-len+1 < 1)
continue;
int val = 0;
for(int j=i-len+1; j<=i; j++)
val =val * 26 + str[j]-'a'+1;
if(vis[val])
bit.add(vis[val],-1);
vis[val] = i;
bit.add(i,1);
for(auto [l,id] : seg[i]) {
l += len - 1;
if(l > i) {
ans[id] = min(ans[id],len);
} else {
int sum = bit.query(i) - bit.query(l-1);
if(sum != base) {
ans[id] = min(ans[id],len);
}
}
}
}
}
for(auto x : ans)
cout<<x<<'\n';
return 0;
}
若你来到南京 是否想起我的梧桐
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1<<17; // 1e5+
// 下标是[0,N-1];
struct SGT{
static const int N = 1<<17; // 1e5+
ll val[N*2], tag[N*2];
void up(int u){
val[u] = max(val[u<<1], val[u<<1|1]);
}
void down(int u){
val[u<<1] += tag[u], val[u<<1|1] += tag[u];
tag[u<<1] += tag[u], tag[u<<1|1] += tag[u];
tag[u] = 0;
}
void mdf(int l, int r, ll x, int u=1, int p=N/2){ // 区间加法 p的真实意义为区间长度的一半。
if(l<=0 && p*2-1<=r) { val[u]+=x, tag[u]+=x; return;}
down(u);
if(l<p) mdf(l,r,x,u<<1,p>>1);
if(r>=p) mdf(l-p,r-p,x,u<<1|1,p>>1);
up(u);
}
ll ask(int l, int r, int u=1, int p=N/2){ // 区间最大值
if(l<=0 && p*2-1<=r) return val[u];
down(u);
ll res=-4e18;
if(l<p) res = max(res, ask(l,r,u<<1,p>>1));
if(r>=p) res = max(res,ask(l-p,r-p,u<<1|1,p>>1));
return res;
}
};
SGT s1,s2;
int main(){
int n; cin>>n;
vector<int> a(n+1), b(n+1), c(n+1);
for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i]; // 树高 价值 代价
auto lsh = a; // 离散化树高
std::sort(lsh.begin(), lsh.end());
lsh.erase(std::unique(lsh.begin(), lsh.end()), lsh.end());
for(auto &x:a) x = std::lower_bound(lsh.begin(), lsh.end(), x) - lsh.begin();
// f1[i] 必选i,且i的左边树高都<=a[i]情况下最优解。
// s1[h] 当前高度为h的树的最优解
vector<ll> f1(n+1);
for(int i=1;i<=n;i++){
f1[i] = s1.ask(0,a[i])+b[i]; // 新的dp值
ll w1 = s1.ask(a[i],a[i]); // 旧的dp值
s1.mdf(a[i],a[i],-w1+max(w1, f1[i])); // 更新dp值
s1.mdf(0,a[i]-1,-c[i]); // 区间修改体现代价
}
vector<ll> f2(n+1);
for(int i=n;i>=1;i--){
f2[i] = s2.ask(0,a[i])+b[i];
ll w2 = s2.ask(a[i],a[i]);
s2.mdf(a[i],a[i],-w2+max(w2,f2[i]));
s2.mdf(0,a[i]-1,-c[i]);
}
ll ans=-4e18;
for(int i=1;i<=n;i++){
ans = max(ans, f1[i]+f2[i]-b[i]);
}
cout << ans <<"\n";
}