牛客寒假训练赛第二场
基本情况
前面过的很顺,F吃满罚时,T4次WA4次最后乱搞过的,K有一点思路,但是码力跟不上,其他没做的题题目基本没思路。
EF
https://ac.nowcoder.com/acm/contest/67742/E
https://ac.nowcoder.com/acm/contest/67742/F
两题虽然都是过了,但一个是提交前改了很久,一个是提交改了很久。
E
思路肯定没别的了,就是我的实现太容易出锅
-
\(\text{mycode}\)
void solve() { int n; cin >> n; vector<int> a(n); for (auto& i : a) cin >> i; if (n == 1) {cout << 1 << endl; return ;} if (count(all(a), 1) == 0 || count(all(a), 1) == n) { cout << n << endl; return ; } int del = 0; int pos = n - 1; while(pos > 0) { int last = pos; while(a[pos] == a[pos - 1] && pos > 0) pos--;//找到第一个和尾部不同的元素 if (pos == 0) {del += last - pos + 1; break;} del++; pos -= 2; if (pos == 0) {del++; break;} } cout << del << endl; }
虽然最后过了,两个死亡
while
而且这个pos -= 2
看起来很不舒服,样例也是调了很久才过。 -
\(STD\)
while(T--) { scanf("%d",&n); for(i=1;i<=n;i++) scanf("%d",&a[i]); ans=0; now=n;//这里多加了变量来维护尾部指针 for(i=n;i;i--) { if(a[i]!=a[now])//就不用像我那样判while套while,不容易出错 { ans++; now=i-1; } } for(i=1;i<=now;i++)//最后再找最后一段的答案。 { if(a[i]!=a[1]) break; ans++; } printf("%d\n",ans); }
F
想了三个解法,前两个超时但是正确性保证,第三个不超时但是部分错误。
最后用第三个解法加上分讨错误部分用第一个解法过了。
总结起来学到几个问题:
- 超时不要慌,先认真分析时间复杂度,看看为什么和预期结果有出入,往往是哪个大的循环不合理,不要一开始就想着优化小细节或者卡常。
- \(\text{set}\) 一定要记得有 \(\text{earse}\) 操作
- 不要在用迭代器遍历这些容器的循环中进行 \(\text{erase}\)
-
\(MyCode\)
void solve() { int n; cin >> n; vector<int> a(n); for (auto& i : a) cin >> i; if (n == 1) {cout << 1 << endl; return ;} set<int> color; vector<int> vis(n + 1); for (int i = 0; i < n; i++) color.insert(a[i]); int color_cmp = sz(color); int p = n - 1; ll ans = 0; int round = 0; int color_cnt = 0, last = 0; while(p > 0) {//大概思路就是找一段刚好达到总出现颜色数量的段,然后取该段最后一个(显然是改颜色的最右边出现的元素)直接删除。 round++; last = p; color_cnt = 0; while(p >= 0 && color_cnt < color_cmp) { if (vis[a[p]] < round) { vis[a[p]] = round; color_cnt++; } p--; } if (color_cnt == color_cmp) ans++; else {//但是这个算法对于最左边那段不适用 p = last; break; } if (p == 0) ans++; } if (p > 0) {//最后剩的一段,不知道怎么用上面方法是实现,就用了一开始比较暴力的方法。 stack<int> pos[n + 1]; color.clear(); for (int i = 0; i <= p; i++) color.insert(a[i]), pos[a[i]].push(i); while(p > 0) { int min_pos = inf; for (auto i : color) { while(!pos[i].empty() && pos[i].top() > p) pos[i].pop(); if (pos[i].empty()) continue; min_pos = min(min_pos, pos[i].top()); pos[i].pop(); } p = min_pos; ans++; } } cout << ans << endl; }
-
\(STD\)
其实就类似于一开始比较暴力的方法,然后再用最后那个部分正确方法的思路优化,先把我一开始的方法贴上来吧
void solve() { int n; cin >> n; vector<int> a(n); for (auto& i : a) cin >> i; if (n == 1) {cout << 1 << endl; return ;} priority_queue<int, vector<int>, greater<int> > q; stack<int> pos[n + 1]; set<int> color; for (int i = 0; i < n; i++) { pos[a[i]].push(i); color.insert(a[i]); } int p = n - 1, ans = 0; while(p > 0) { int min_pos = inf; for (auto i : color) {//无脑把所有不符合的元素都出栈 while(!pos[i].empty() && pos[i].top() > p) pos[i].pop(); if (pos[i].empty()) continue; min_pos = min(min_pos, pos[i].top()); pos[i].pop(); } p = min_pos; ans++; } cout << ans << endl; }
就是把每个颜色对应的每个位置入栈,然后每次出栈最栈顶靠前的颜色。
但是这里我每轮都进行了把位置在当前位置之后的元素都出栈的操作,会超时。
实际上可以通过类似我最后的方法,针对区间处理,每次删完元素就把处理区间转移到 \([找到新的元素的位置,这个元素左边]\)
void solve() { int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; i++) cin >> a[i]; stack<int> pos[n + 2]; for (int i = 1; i <= n; i++) pos[a[i]].push(i); int end_pos = n, ans = 0; for (int i = 1; i <= n; i++) if (!pos[i].empty()){ end_pos = min(end_pos, pos[i].top()); } int now = end_pos, pre = n;//now是当前要操作的颜色位置,pre是这个区间的右端 while(pre >= 1) { ans++; end_pos = now; for (int i = pre; i >= now; i--) {//把当前区间的颜色都出栈一次,因为操作的是now(end_pos),显然它后面的元素都被删了 pos[a[i]].pop(); if (!pos[a[i]].empty()) end_pos = min(end_pos, pos[a[i]].top()); } pre = now - 1;//当前区间最右端变成了操作位置的左端,显然 now = end_pos; } cout << ans << endl; }
-
\(MycodePlus\)
下午分析了一下我第一个做法,理论上,时间复杂度明明是对的?
仔细看了一下,发现一个重要问题,我每次跑 while
都要无端遍历一下 color
,但实际上很多颜色已经全部删掉了,注意这里的颜色数是可以到达 \(2\times 10 ^ 5\) 的,即便我代码中作了如下处理:
if (pos[i].empty()) continue;//我潜意识下把这一句话等同于删除了颜色
这里我的理想状态下,这些不用的颜色自然就 continue
遍历是不耗时的,然而这就是导致我超时的关键原因!事实上光跑一个 \(for\) 都是耗时的,更何况我加入了判断语句,只要一步多哪怕一点时间,只要不客观上减少 \(color\) 的大小,就会多 \(t\times n^2\) 的时间。
set
开来做什么用的?可以直接删除没用的颜色啊!
然而直接这样搞运行时错误
for (auto i : color) {//无脑把所有不符合的元素都出栈
while(!pos[i].empty() && pos[i].top() > p) pos[i].pop();
if (pos[i].empty()) {
color.erase(i);
continue;
}
min_pos = min(min_pos, pos[i].top());
pos[i].pop();
}
调了二十分钟,突然想到如果我把这个元素删了,set
循环遍历下一个元素毕竟是指针操作,可能指向下一个元素的指针也乱了之类,所以似乎根本没法在循环里面实现边循环边删除,于是我就嗯造了一个数组存要删除的元素(常数接着炸)
while(p > 0) {
int min_pos = inf;
vector<int> del;//存要删除的值
for (auto& i : color) {
while(!pos[i].empty() && pos[i].top() > p) pos[i].pop();
if (pos[i].empty()) {
del.emplace_back(i);
continue;
}
min_pos = min(min_pos, pos[i].top());
pos[i].pop();
}
for (auto& i : del) {
color.erase(i);
}
p = min_pos;
ans++;
}
令人忍俊不禁的是,这居然就过了。
void solve() {
int n;
cin >> n;
vector<int> a(n);
for (auto& i : a) cin >> i;
if (n == 1) {cout << 1 << endl; return ;}
priority_queue<int, vector<int>, greater<int> > q;
stack<int> pos[n + 1];
set<int> color;
for (int i = 0; i < n; i++) {
pos[a[i]].push(i);
color.insert(a[i]);
}
int p = n - 1, ans = 0;
while(p > 0) {
int min_pos = inf;
vector<int> del;
for (auto& i : color) {
while(!pos[i].empty() && pos[i].top() > p) pos[i].pop();
if (pos[i].empty()) {
del.emplace_back(i);
continue;
}
min_pos = min(min_pos, pos[i].top());
pos[i].pop();
}
for (auto& i : del) {
color.erase(i);
}
p = min_pos;
ans++;
}
cout << ans << endl;
}
最后想想也确实,每次把没有的颜色删掉,而颜色的数量很大,本身就是影响时间复杂度的最大因素。
K
https://ac.nowcoder.com/acm/contest/67742/K
想的是爆搜,我码力不够。
-
先贴上 \(STD\) 的爆搜
const int MAX=5e6+10; const ll mod=1e9+7; int n,used[12]; char a[MAX],b[MAX],to[12]; ll dfs(int x,int now,int ok) { if(x==n+1) return now==0; if(a[x]>='0' && a[x]<='9') { if(!ok && a[x]>b[x]) return 0; return dfs(x+1,(now*10+a[x]-'0')%8,ok|(a[x]<b[x])); } int i; ll res=0; if(a[x]>='a' && a[x]<='j') { if(to[a[x]-'a']!='#') { if(!ok && to[a[x]-'a']>b[x]) return 0; return dfs(x+1,(now*10+to[a[x]-'a']-'0')%8,ok|(to[a[x]-'a']<b[x])); } else { for(i=((x==1&&n>1)?1:0);i<=9;i++) { if(used[i]) continue; if(!ok && i>b[x]-'0') continue; used[i]=1; to[a[x]-'a']=i+'0'; res=(res+dfs(x+1,(now*10+i)%8,ok|(i<b[x]-'0')))%mod; used[i]=0; to[a[x]-'a']='#'; } } } else if(a[x]=='_') { for(i=((x==1&&n>1)?1:0);i<=(ok?9:b[x]-'0');i++) { res=(res+dfs(x+1,(now*10+i)%8,ok|(i<b[x]-'0')))%mod; } } return res; } int main() { int t,i; scanf("%d",&t); while(t--) { scanf("%d",&n); scanf("%s",a+1); scanf("%s",b+1); memset(to,'#',sizeof to); memset(used,0,sizeof used); if(a[1]=='0') { if(n>1) puts("0"); else puts("1"); } else printf("%lld\n",dfs(1,0,0)); } return 0; }
但实际上,仅针对这个 \(easy\) 版本来说,可以考虑从枚举全集入手。
先分析题目条件来想怎么枚举:
-
同字母对应的数码一定相同,不同字母对应的数码一定不同
- 这里我想的是搞暴力枚举然后检查是不是相同,并不直接
- 可以直接从这个性质下手枚举,对 \(a,b,c,d\) 应该赋的值进行枚举,然后通过这个构造数字,就必然符合要求条件。
-
不同字母对应的数码一定不相同
- 接上文,通过 set 来对枚举出来的 \(a,b,c,d\) 去重即可。
-
超时?
- 枚举五个数码加上对原数组逐位带入。
void solve() { int n; string s; ll x = 0, y = 0; cin >> n >> s >> y; set<ll> ans; auto calc = [&](ll x) { if (x == 0) return 1; int cnt = 0; for (; x > 0; x /= 10) cnt++; return cnt; }; for (int a = 0; a < 10; a++) for (int b = 0; b < 10; b++) for (int c = 0; c < 10; c++) for (int d = 0; d < 10; d++) { set<int> cmp{a, b, c, d}; if (cmp.size() < 4) continue; for (int _ = 0; _ < 10; _++) { x = 0; for(char& ch : s) { switch (ch) { case 'a': x = x * 10 + a; break; case 'b': x = x * 10 + b; break; case 'c': x = x * 10 + c; break; case 'd': x = x * 10 + d; break; case '_': x = x * 10 + _; break; default: x = x * 10 + (ch ^ 48); break; } } if (calc(x) == n && x <= y && x % 8 == 0) ans.insert(x); } } cout << ans.size() << endl; }
C
https://ac.nowcoder.com/acm/contest/67742/C
01字典树,逆元
- 依题意,只有子序列的最大值和最小值对答案产生贡献。
- 对 \(a\) 数组排序,先选定最大值和最小值。
- 满足题意得最大值和最小值之间一共有 \(k\) 个数的话(不包括这两个数),方案数就是 \(2^k\)
但是暴力枚举会超时。
-
位运算的特殊性
- 按位运算,互相之间不影响。
- 枚举最大值,看有哪些最小值符合小于等于 \(k\)
-
通过01字典树枚举
-
01字典树从左到右肯定是递增的
- 选好最大值,统计最大值左边的枝干
-
字典树同一个枝干的两个数在枝干上的位异或和肯定是 \(0\)。
-
按小到大对数编号,因为方案数是 \(2^k\) , 而 \(k = i - j - 1\) 其中 \(a_i\) 为最大值,\(a_j\) 为要找的符合条件的最小值,\(i - j - 1\) 为除去它两之外的区间元素数目。
-
-
对于一个 \(a_j\),它贡献的答案为 \(2^{i-j-1}\)。我们可以将这个式子拆掉: \(2^{i-j-1}=\frac{2^{i-1}}{2^{j}}\),于是 \(i\) 与 \(j\) 就分离了。所以我们可以把 \(v_i = \frac {1} {2^{i}}\) 插入 。当 \(max=a_i\) 时,在 Trie 中查询 \(\sum v_j (a_j \oplus a_i \leq k)\),此时贡献为 \(1 + 2^{i-1} \cdot \sum v_j\)。
\(2^i\) 与 \(\frac {1}{2^i}\) 都可以预处理求出 (\(\frac {1}{2^i}\) 要用到逆元)。然后枚举 \(max\),每次在 Trie 中查询,总时间复杂度为 \(O(n \log n)\)。
-
因为该题模数是质数,可以直接用费马小定理推得逆元为 \(a^{p - 2}\)
- 快速幂边算边取模即可。
int tot;
i64 t[N][2], w[N];
void solve() {
int n, k;
std::cin >> n >> k;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
std::cin >> a[i];
auto init = [&]() {
for (int i = 0; i <= tot; i++)
t[i][0] = t[i][1] = w[i] = 0;
tot = 0;
};
auto updata = [&](i64 x, int id) {
i64 u = 0, inv = qpow(qpow(2, id), mod - 2);
for (int i = 30; i >= 0; --i) {
i64 e = (x >> i) & 1;
if (!t[u][e])
t[u][e] = ++tot;
u = t[u][e], w[u] = (w[u] + inv) % mod;
}
};
auto query = [&](i64 x) {
i64 u = 0, res = 0;
for (int i = 30; i >= 0; --i) {
i64 e = ((x >> i) & 1) ^ ((k >> i) & 1);
if ((k >> i) & 1)
res = (res + w[t[u][1 - e]]) % mod;
if (!t[u][e])
return res;
u = t[u][e];
}
return (i64)(res + w[u]) % mod;
};
std::sort(all1(a));
i64 ans = 0;
for (int i = 1; i <= n; i++) {
ans = (ans + query(a[i]) * qpow(2, i - 1) % mod + 1) % mod;
updata(a[i], i);
}
std::cout << ans << '\n';
init();
}
GH
https://ac.nowcoder.com/acm/contest/67742/G
https://ac.nowcoder.com/acm/contest/67742/H
线段树及其进阶应用
G
显然是一个数据结构题,需要支持两种操作:
-
单点修改
-
区间查询,在查询的区间中选出一个子段,再把子段切成两部分,求前一个部分的和减去后一个部分的和的最大值。
对于查询操作,有两个问题:
- 怎么选子段?
- 把子段切成两部分,在哪切?
可以看到 easy 版本中数字都是非负数,稍加思考可以得到两个显然的贪心结论:
- 假设我们已经选出了一个子段 \([x,y]\),那答案必定为 \(\sum_{i=x}^{y-1} a_i - a_y\),也就是说 easy 版本省去了问题2 "在哪切"。
- 再多思考一下会发现,问题1 "怎么选" 也得到了简化。因为数字都是非负数,查询求的是前一段和-后一段和最大,所以前一段肯定尽可能长。那么对于查询的区间 \([L,R]\) 来说,肯定选 \([L,y](L+1 \leq y \leq R)\) 这个子段,那答案就变成了 \(\sum_{i=L}^{y-1} a_i - a_y\)。也就是说只要确定了 \(y\),就能得出答案。
根据上述两个结论,现在考虑如何求答案。
考虑维护区间中每个 \(y\) 的答案 \(ans_y\)。我们发现答案的一部分由区间和构成,所以我们先用前缀和对答案的式子做一些化简变形:
令 \(bit_i=\sum_{j=1}^{i} a_j\)
\(\sum_{i=L}^{y-1} a_i - a_y\)
\(=\sum_{i=L}^{y} a_i - 2 \cdot a_y\)
\(=\sum_{i=1}^{y} a_i - \sum_{i=1}^{L-1} a_i - 2 \cdot a_y\)
\(=bit_y - bit_{L-1} - 2 \cdot a_y\)
但这个式子还与 \(L\) 有关,无法直接用一个变量维护它的最大值,于是我们可以分开维护:
- 维护 \(mx\):\(mx_y=bit_y - 2 \cdot a_y\)
- 维护 \(bit\)
于是 \(ans= \max_{i=L+1}^{R} mx_i - bit_{L-1}\)
注意此时单点修改 \(a_x\) 会对区间 \([x,n]\) 中的 \(mx\) 产生修改,所以其实是个区间修改。
我们可以使用线段树维护,支持 "区间加","区间求 \(max\)" 即可。
时间复杂度 \(O(q \log n)\)
struct SegmentTree {
#define type i64
#define ls (id << 1)
#define rs (id << 1 | 1)
struct node {
type max, sum;
void init() {
max = -LINF;
sum = 0;
}
}t[N << 2];
int n, ql, qr, qop;
type a[N], bit[N], tag[N << 2], qv;
node merge(node x, node y) {//维护最大值和区间和
node res;
res.sum = x.sum + y.sum;
res.max = std::max(x.max, y.max);
return res;
}
void pushUp(int id) {t[id] = merge(t[ls], t[rs]);}//合并到父亲
void pushDown(int l, int r, int id) {//lazyTag下放
if (!tag[id]) return ;
t[ls].max += tag[id];
t[rs].max += tag[id];
tag[ls] += tag[id]; tag[rs] += tag[id];
tag[id] = 0;
}
void build(int l, int r, int id) {
tag[id] = 0; t[id].init();
if (l == r) {
t[id].max = bit[l] - 2 * a[l];
t[id].sum = a[l];
return ;
}
int mid = l + r >> 1;
build(l, mid, ls);
build(mid + 1, r, rs);
pushUp(id);
}
void upDate(int l, int r, int id) {
if (l >= ql && r <= qr) {
if (qop == 1) t[id].sum = qv;//维护区间和
else {//维护区间上述式子的最大值
t[id].max += qv;
tag[id] += qv;
}
return ;
}
pushDown(l, r, id);
int mid = l + r >> 1;
if (ql <= mid) upDate(l, mid, ls);
if (qr > mid) upDate(mid + 1, r, rs);
pushUp(id);
}
node query(int l,int r,int id) {
if(l >= ql && r <= qr) return t[id] ;
pushDown(l, r, id);
int mid = l + r >> 1;
if(qr <= mid) return query(l, mid, ls);
if(ql > mid) return query(mid + 1, r, rs);
return merge(query(l, mid, ls),query(mid + 1, r, rs)) ;
}
void build(int _n){ n = _n; build(1, n, 1) ;}
void upd(int l,int r,type v,int op) {
ql = l; qr = r; qv = v; qop = op;
upDate(1, n, 1);
}
type ask_max(int l,int r) {
ql = l; qr = r;
return query(1, n, 1).max ;
}
type ask_sum(int l,int r) {
if(l > r) return type(0);
ql = l; qr = r;
return query(1, n, 1).sum ;
}
#undef type
#undef ls
#undef rs
}T;
i64 a[N];
void solve() {
int n, q;
std::cin >> n >> q;
T.bit[0] = 0;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
T.a[i] = a[i]; T.bit[i] = T.bit[i - 1] + a[i];
}
T.build(n);
while(q--) {
i64 opt, x, y; std::cin >> opt >> x >> y;
if (opt == 1) {
T.upd(x, x, 2 * a[x] - 2 * y, 2);//修改该区间上述式子的最大值
T.upd(x, n, y - a[x], 2);//修改后续最大值
a[x] = y;
T.upd(x, x, a[x], 1);//维护区间和
}
else {
std::cout << T.ask_max(x + 1, y) - T.ask_sum(1, x - 1) << '\n';
}
}
}