杂记
Bennyxu2024 · · 算法·理论
摘要:这个文章是自己刷题的时候的错误大赏以及有趣思路记录
1:杂谈
有的时候会出现
2:
输入循环要因题而异
3:
暴力是可以倍增优化的,例如P10976
在遇到直接
但是如果问题是动态的,会出现修改,那么则无法使用
此外,如果询问非常少,那么
4:
memset(dis, 0x3f, sizeof(dis));
for(int i = 1; i <= n; i++)dis[i][i] = 0;
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
chkmin(dis[i][j], dis[i][k] + dis[k][j]);
最外层枚举中转点,
2:
数位dp一般写记忆化,方便理解,并且在受到限制的时候不做记忆化,否则会有统计错误
可以简单理解说记忆化就是不受限制,否则也无法记忆化
3: kmp
这个算法的kmp数组本质是最长公共前后缀,所以也可以做最短的公共非空前后缀
所以kmp数组也是可以作各种字符串的循环周期的
4: Tarjain
5:单调队列
先入队,再出队,不会有错
端点定义如下:
6:单调栈
同上,也需要注意题目让你干什么
7:SPFA找负环
memset(d, 0x3f, sizeof(d));
memset(cnt, 0, sizeof(cnt));
memset(vis, false, sizeof(vis));
queue<int> q;
q.push(1);d[1] = 0;vis[1] = true;//1开始,但如果不联通记得循环一下
while(!q.empty()){
int u = q.front(); q.pop();
vis[u] = false;
for(auto ed : e[u]){
int v = ed.v, w = ed.w;
if(w + d[u] < d[v]){
d[v] = w + d[u];
if(!vis[v]){
if(++cnt[v] >= n){
cout << "YES\n";//找到了
return;
}
q.push(v);
vis[v] = true;
}
}
}
}
cout << "NO\n";//不存在负环
8:线段树合并
WA,20pts:在线段树合并的时候,需要有 root[u] = merge(u, v, ...),因为可能 u 是空节点,需要将非空节点 v 传到节点 u 这里,也就是root[u] = root[v]。
WA,50pts:需要在合并的 dfs 里就把答案记录下来,如上一点所说,可能会被覆盖。比如 u 是空节点,就会令 root[u] = root[v],然后后续的有关 u 其它儿子的合并,就会把本来 v 节点的值给覆盖掉,所以要拿个数组记录。
线段树空间至少要开到
参考代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN = 1e5 + 5;
struct Seg{
ll ls, rs, ans, cnt;
}seg[MAXN * 80];
ll segcnt;
ll New(){
return ++segcnt;
}
void pushup(ll x){
if(seg[x].ls == 0){
seg[x].ans = seg[seg[x].rs].ans;
seg[x].cnt = seg[seg[x].rs].cnt;
return;
}
if(seg[x].rs == 0){
seg[x].ans = seg[seg[x].ls].ans;
seg[x].cnt = seg[seg[x].ls].cnt;
return;
}
if(seg[seg[x].ls].cnt >= seg[seg[x].rs].cnt){
seg[x].ans = seg[seg[x].ls].ans;
seg[x].cnt = seg[seg[x].ls].cnt;
}
else{
seg[x].ans = seg[seg[x].rs].ans;
seg[x].cnt = seg[seg[x].rs].cnt;
}
return;
}
void insert(ll& x, ll l, ll r, ll a, ll val){//a += val
if(!x)x = New();
if(l == r){
// cout << x << ' ' << a << endl;
seg[x].cnt += val;
seg[x].ans = a;
return;
}
ll mid = (l + r) >> 1;
if(a <= mid){
// if(seg[x].ls == 0)seg[x].ls = New();
insert(seg[x].ls, l, mid, a, val);
}
else{
// if(seg[x].rs == 0)seg[x].rs = New();
insert(seg[x].rs, mid + 1, r, a, val);
}
pushup(x);
}
ll root[MAXN], ans[MAXN];
ll merge(ll u, ll v, ll l, ll r){
if(u == 0 || v == 0)return u + v;
if(l == r){
seg[u].ans = l;
seg[u].cnt += seg[v].cnt;
return u;
}
ll mid = (l + r) >> 1;
seg[u].ls = merge(seg[u].ls, seg[v].ls, l, mid);
seg[u].rs = merge(seg[u].rs, seg[v].rs, mid + 1, r);
pushup(u);
return u;
}
ll n, m;
int st[MAXN][20], d[MAXN];
vector<int> e[MAXN];
void dfs(ll u, ll fa){
d[u] = d[fa] + 1;
st[u][0] = fa;
for(auto v : e[u])if(v != fa)dfs(v, u);
}
int lca(ll x, ll y){
if(d[x] != d[y]){
if(d[x] < d[y])swap(x, y);
// ll dif = d[x] - d[y];
for(int i = 19; i >= 0; i--)if(d[st[x][i]] >= d[y])x = st[x][i];
}
if(x == y)return x;
for(int i = 19; i >= 0; i--)if(st[x][i] != st[y][i]){x = st[x][i], y = st[y][i];}
return st[x][0];
}
void dfs2(ll u, ll fa){
for(auto v : e[u])if(v != fa){
dfs2(v, u);
root[u] = merge(root[u], root[v], 1, MAXN);
}
ans[u] = (seg[root[u]].cnt == 0 ? 0 : seg[root[u]].ans);
}
signed main(){
cin.tie(0) -> sync_with_stdio(false);
cin >> n >> m;
for(int i = 1, u, v; i < n; i++){
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
// root[i] = New();
}
// root[n] = New();
dfs(1, 0);
for(int i = 1; i <= 19; i++)
for(int j = 1; j <= n; j++)
st[j][i] = st[st[j][i - 1]][i - 1];
for(int i = 1, u, v, z, l, fl; i <= m; i++){
cin >> u >> v >> z;
l = lca(u, v);fl = st[l][0];
insert(root[u], 1, MAXN, z, 1);
insert(root[v], 1, MAXN, z, 1);
insert(root[l], 1, MAXN, z, -1);
insert(root[fl], 1, MAXN, z, -1);
}
dfs2(1, 0);
for(int i = 1; i <= n; i++)cout << ans[i] << '\n';
return 0;
}
3: 神秘数学:
Prufer序列
建立无根树到n-2序列的双射关系
也就是说,有两两对应关系
生成Prufer序列源码:
for(int i = 1; i < n; i++){
cin >> f[i];
d[f[i]]++;
}
for(int i = 1, j = 1; i <= n - 2; i++, j++){
while(d[j])++j;
p[i] = f[j];
while(i < n && !--d[p[i]] && p[i] < j)p[i + 1] = f[p[i]], ++i;
}
解码Prufer序列源码:
for(int i = 1; i <= n - 2; i++){
cin >> p[i];
d[p[i]]++;
}
p[n - 1] = n;
for(int i = 1, j = 1; i < n; i++, j++){
while(d[j])++j;
f[j] = p[i];
while(i < n && !--d[p[i]] && p[i] < j)f[p[i]] = p[i + 1], ++i;
}
作用:各种计数
3:比赛策略
NOIP和CSP-S特殊性质会有正解提示性。
可以打多个暴力,进行判断
NOIP现阶段策略:因为菜,选择NOIP4.5h凑200+pts(大概T1AC,T2AC,T3&T4拿暴力)
看题解不同解法也是重要的
利用assert调试
多函数调试,手算确定正确值传入,进行调试
计数题对拍(从大样例弄出来一些,因为可能有corner case)
提交之前一定在Lemon测试,保证行为一致
Windows下Linux虚拟机速度受限
NOIP若通过大样例,约等于通过,挂分应该不会太多的吧……
4: 优秀trick
P4643 图论边权拆分trick
边权可以拆为
考虑差,可知都选上时贡献存在,各选一个贡献抵消
按新点权贪心即可
P3953 图论反图dfs等价拓扑排序转移trick
这题有一个重要的点在于求解是否有一种方案会经过0环,并且还需要维护方案数的dp
如果采用拓扑排序,容易发现并不好写
介绍trick:建反图跑记忆化搜索,如果有遇到已经访问过的点,并且是路径长度也相等的情况下,这表明我们经过了一个0环,可以break了
并且在反图上dfs等价拓扑序,是可做的。
介绍性质:最短路由最短路拼接,否则不为最短路
正确性显然
P12448 数据结构拆分 \max trick
原题:
给定序列
单点加1修改
上式查询
trick:拆分max
先把和式改为
再设a上界为
再进一步预处理出
此时题意转化为:
求大于一定值的len的数量与和,单点修改&区间查询,树状数组可做
求一个极大区间max恰好为a[k]的左端点和右端点,线段树二分可做
初始化的时候用单调栈预处理len即可,
V上限是
P5664 合理的dp状态优化以及容斥原理
不多于
考虑反过来,要求i出现超过k/2次
可是设计出一种
但是并不能完美通过此题
其实本质上我们只是关心i和非i之间的大小关系, 具体是多少无人在意
所以把两维压缩为一维,
做完了
P14638 NOIPT4 区间在二维空间上的映射,分治的提示
可以把一个区间转化为一个二维平面上的点
画出图之后不难发现只有
题意转化为若干个在(i, i)右下的可选用点求和
可以去掉
对于每一个点,考虑特殊性质D和E,分别为
考虑分治
分治的话可以先将整个坐标系增加到
对于