继续早睡早起锻炼身体
规划好时间
这周就割点和桥,一场个人比赛
周一 4.12(割点和桥)
「一本通 3.6 例 1」分离的路径 (边双联通分量缩点)
换了一种写法,很类似Tarjan的缩点的写法
这样就不用真的去写删边,搜索的过程中就顺便求出来了
这样写代码会简洁一些
注意最后还剩下根节点那个联通分量,要注意
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 5e3 + 10;
int node[N], deg[N], st[N], num, top;
int dfn[N], low[N], cnt, n, m;
vector<int> g[N];
void dfs(int u, int fa)
{
dfn[u] = low[u] = ++cnt;
st[++top] = u;
bool vis = false;
for(auto v: g[u])
{
if(!dfn[v])
{
dfs(v, u);
low[u] = min(low[u], low[v]);
if(low[v] > dfn[u])
{
num++;
while(1)
{
node[st[top--]] = num;
if(st[top + 1] == v) break;
}
}
}
else
{
if(v == fa && !vis) vis = true;
else low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, m)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
num++;
while(top) node[st[top--]] = num;
_for(u, 1, n)
for(auto v: g[u])
if(node[u] > node[v])
deg[node[u]]++, deg[node[v]]++;
int sum = 0;
_for(i, 1, num)
sum += deg[i] == 1;
printf("%d\n", (sum + 1) / 2);
return 0;
}
周二 4.13(割点和桥)
昨天今天训练时间是够的,只是一直卡在一道题上,所以从题量上看上去比较少
[HNOI2012]矿场搭建(点双联通分量)
这题搞了我好久
首先要明确一个概念
点双联通分量也叫块。块和边双联通分量不一样,边双联通分量是去掉割边之后就是边双联通分量
而块中,一个割点一定属于两个以上的块,两个块的公共点是割点
怎么求呢
可以边tarjan边求,边搜边把点入栈,然后遇到割点就出栈一直到割点的前一个点
这个时候割点要记入这个块中,但不要出栈,因为割点属于2个以上的块,后面的块还会包括这个割点
这里还有一个问题,就是根节点如果不是割点的话,它是不会入栈的,最后会剩下来
这和求边双联通分量的时候还剩下一个一样。
所以要特判一下。
但其实这种写法挺麻烦的,还有一种更简单的方法,就是分开求,先求出割点
然后dfs,每次找一个非割点的点来dfs,遍历这个点所在的块,这时注意不能搜到其他块里面,而两个块的公共点就是割点,所以
遇到割点的时候就不要继续搜下去了。
回到这道题
这道题说点没了,那显然和割点有关
我们考虑每一个块
如果没有割点,那就随意设立两个点,方案是C(n,2)
如果有一个割点,那么如果这个割点没了,这个块中的其他点就和其他部分分开了,所以这个块的非割点要设立一个,方案数为块的节点数-1,也就是去掉割点
如果有两个割点,去掉一个割点,其他点可以通过另一个割点跑到其他的块中,而对于另一个块,如果这个公共割点是它唯一的割点,它会设立一个,如果不是,那么可以继续从其他块的另一个割点跑到第3个块
第3个块同样,这样迭代下去是可以获救的。因此如果有两个割点就不用设立
然后这题实现的时候也有很多细节要注意。我们需要算出每个块的割点数以及节点数
每一个块中,每一个割点只能访问一次,访问多了节点数割点数就不对了。
为了避免每次都memset数组,我就用一个unordered_map来记录
还有防止重复遍历节点的vis数组不能标记割点,因为其他块还会搜到割点
细节要注意
我写程序时一些变量名要表示它的作用
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e4;
int dfn[N], low[N], cnt, n, m, root;
int st[N], cut[N], vis[N], num, top, node, sum;
unordered_map<int, bool> vis_cut;
vector<int> g[N];
void tarjan(int u) //求割点
{
dfn[u] = low[u] = ++cnt;
st[++top] = u;
int son = 0;
for(int v: g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if((u == root && ++son > 1) || (u != root && low[v] >= dfn[u])) cut[u] = 1;
}
else low[u] = min(low[u], dfn[v]); //已经出栈的点不要算
} //无向图没有横叉边,不写也没关系。
}
void dfs(int u)
{
vis[u] = 1; node++;
for(int v: g[u])
{
if(vis[v]) continue;
if(cut[v])
{
if(!vis_cut[v])
vis_cut[v] = 1, node++, sum++;
}
else dfs(v);
}
}
int main()
{
int kase = 0;
while(scanf("%d", &m) && m)
{
num = top = cnt = n = 0;
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(cut, 0, sizeof(cut));
memset(vis, 0, sizeof(vis));
_for(i, 1, N - 1) g[i].clear();
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
n = max(n, max(u, v));
}
_for(i, 1, n)
if(!dfn[i]) //可能不连通,dfs时要小心
tarjan(root = i);
long long res = 1, t = 0;
_for(i, 1, n)
if(!vis[i] && !cut[i])
{
node = sum = 0;
vis_cut.clear();
dfs(i);
if(sum == 0) t += 2, res *= node * (node - 1) / 2;
if(sum == 1) t++, res *= (node - 1);
}
printf("Case %d: %lld %lld\n", ++kase, t, res);
}
return 0;
}
「一本通 3.6 练习 1」网络(割点模板题)
注意两个点
(1)割点判断条件low[v] >= dfn[u]不要写错
(2)同一个割点不同的儿子时可能被判断多次,所以不能用计数器++的方式算总的割点数目
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 110;
int dfn[N], low[N], cut[N], cnt, n, root;
vector<int> g[N];
void tarjan(int u)
{
dfn[u] = low[u] = ++cnt;
int son = 0;
for(int v: g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(u == root && ++son > 1 || u != root && low[v] >= dfn[u]) cut[u] = 1; //注意一个割点会被判断多次
}
else low[u] = min(low[u], dfn[v]);
}
}
int main()
{
while(scanf("%d", &n) && n)
{
_for(i, 1, n) g[i].clear();
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(cut, 0, sizeof(cut));
cnt = 0;
int u, v;
while(scanf("%d", &u) && u)
while(1)
{
scanf("%d", &v);
g[u].push_back(v);
g[v].push_back(u);
if(getchar() == '\n') break;
}
_for(i, 1, n)
if(!dfn[i])
tarjan(root = i);
int ans = 0;
_for(i, 1, n)
ans += cut[i];
printf("%d\n", ans);
}
return 0;
}
周三 4.14(割点和桥)
今天的训练时间就是一个下午,一个下午都再写一道题
P5058 [ZJOI2004]嗅探器(求割点拓展)
按照题目的意思,两个点的任何路径都要经过所求的点
那么我马上就想到这个点是割点,然后两个块之间的公共点是这个割点,a,b属于不同的块
然后就这么写了,结果80分
一直不知道哪里错了,后来发觉可以a,b所在的块可以不相邻,可以中间经过几个割点
这么一搞就很难弄了,我尝试了把一个块缩成一个点,要给割点缩成一条边的方式,发现实现起来很麻烦,因为会有连续几个割点的情况。
后来看了题解,发现题解非常巧妙,是在tarjan的过程中顺便就求出了
而没有往块这个方向想
理一下思路,首先所求点为一个割点,但这个割点去掉以后一定要使得a,b不联通
所以我们从a开始tarjan,然后搜到一个割点的时候(除了根节点)
我们考虑这个割点会不会导致a,b不联通
u的儿子是v,u是割点,如果去掉u,就会使得v这颗子树和u父亲的那一个部分分离。
因此如果要使得a,b不联通,那么b一定在v或v的子树当中
如果b 为v 则dfn[b] = dfn[v]
如果b在v的子树中,则dfn[b] > dfn[v]
注意我们判断割点的时候,一定是搜到v的子树中回溯回来的。
所以这个时候判断就一定使得满足dfn[b] > dfn[v],b一定在v的子树当中
所以综合一下就是dfn[b] >= dfn[v]
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
int dfn[N], low[N], cut[N];
int cnt, root, n, a, b;
vector<int> g[N];
void tarjan(int u)
{
dfn[u] = low[u] = ++cnt;
for(int v: g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(u != root && low[v] >= dfn[u] && dfn[b] >= dfn[v]) cut[u] = 1;
}
else low[u] = min(low[u], dfn[v]);
}
}
int main()
{
scanf("%d", &n);
int u, v;
while(scanf("%d%d", &u, &v) && u && v)
{
g[u].push_back(v);
g[v].push_back(u);
}
scanf("%d%d", &a, &b);
tarjan(root = a);
_for(i, 1, n)
if(cut[i])
{
printf("%d\n", i);
return 0;
}
puts("No solution");
return 0;
}
花10分钟秒了一道割边模板题
「一本通 3.6 练习 3」旅游航道(割边模板题)
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 3e4 + 10;
int dfn[N], low[N];
int cnt, n, m, ans;
vector<int> g[N];
void tarjan(int u, int fa)
{
dfn[u] = low[u] = ++cnt;
bool vis = false; //重边
for(int v: g[u])
{
if(!dfn[v])
{
tarjan(v, u);
low[u] = min(low[u], low[v]);
if(low[v] > dfn[u]) ans++;
}
else
{
if(v == fa && !vis) vis = true;
else low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
while(scanf("%d%d", &n, &m) && n && m)
{
cnt = ans = 0;
_for(i, 1, n) dfn[i] = low[i] = 0, g[i].clear();
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
tarjan(1, 0);
printf("%d\n", ans);
}
return 0;
}
周四 4.15(割点和桥)
「一本通 3.6 练习 4」电力(割点能分割的联通分量数)
有了昨天那一题的启发,这题就很容易了,在tarjan的过程中统计就好
这个割点能确定一次,就说明能分离一个联通分量,如果它被确定了第二次,说明又可以多一个联通分量
所以我们就先统计原来的联通分量个数
然后统计每个割点去掉能多几个联通分量,取最大值就好了
注意这道题有个坑点就是边数可以为0,我看题时没注意到
所以输入的时候要注意,还要特判一下
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e4 + 10;
int dfn[N], low[N], ans[N], root, cnt, n, m;
vector<int> g[N];
void tarjan(int u)
{
dfn[u] = low[u] = ++cnt;
int son = 0;
for(int v: g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(u == root && ++son > 1 || u != root && low[v] >= dfn[u]) ans[u]++;
}
else low[u] = min(low[u], dfn[v]);
}
}
int main()
{
while(scanf("%d%d", &n, &m))
{
if(n == 0 && m == 0) break;
cnt = 0;
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(ans, 0, sizeof(ans));
_for(i, 1, n) g[i].clear();
if(m == 0)
{
printf("%d\n", n - 1);
continue;
}
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
u++; v++;
g[u].push_back(v);
g[v].push_back(u);
}
int sum = 0;
_for(i, 1, n)
if(!dfn[i])
{
sum++;
tarjan(root = i);
}
int mx = 0;
_for(i, 1, n)
mx = max(mx, ans[i]);
printf("%d\n", mx + sum);
}
return 0;
}
「一本通 3.6 练习 5」Blockade(tarjan求割点拓展)
和上一题大同小异吧,再拓展一下
首先一个割点去掉,一定会有2 * (n - 1)个点对没了,因为这个点本身没了
然后看分割成几个联通分量,设每个联通分量点的个数为num
没有的点对数为A(n - 1, 2) - A(num1, 2) - A(num2, 2)……
为了求联通分量点的个数,我们就可以dfs一遍求num[u]表示以u为根的子树的节点个数
最后注意根节点求联通分量个数要特判一下
这样一本通的这个专题就刷完了,我的效率还是很ok的
接下来做一波cf杂题
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int dfn[N], low[N], sum[N], root, cnt, n, m;
vector<int> g[N], ans[N];
ll res[N];
void tarjan(int u)
{
dfn[u] = low[u] = ++cnt;
for(int v: g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(u == root) ans[u].push_back(sum[v]);
if(u != root && low[v] >= dfn[u]) ans[u].push_back(sum[v]);
}
else low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u)
{
sum[u] = 1;
for(int v: g[u])
if(!sum[v])
{
dfs(v);
sum[u] += sum[v];
}
}
ll cal(int x) { return 1ll * x * (x - 1); }
int main()
{
scanf("%d%d", &n, &m);
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1);
tarjan(root = 1);
if(ans[root].size() > 1)
{
res[1] = cal(n - 1);
for(int num: ans[root])
res[1] -= cal(num);
}
_for(i, 2, n)
if(ans[i].size())
{
res[i] = cal(n - 1); ll t = 0;
for(int num: ans[i])
{
res[i] -= cal(num);
t += num;
}
res[i] -= cal(n - 1 - t);
}
_for(i, 1, n)
printf("%lld\n", res[i] + (n - 1) * 2);
return 0;
}
周五 4.16 (cf1900杂题)
D. Fill The Bag(思维)
这道题没做出来,是看题解的
我想的时候纠结相加起来1的位置改变的问题,不知道怎么搞
其实这正是关键,这提醒我们可以枚举1的位置,从小到大
接下来就贪心了
我们分bag的1和box的1
对于当前位
如果bag不需要放,那么我们就把box当前位的1相加,给到下一位
如果需要放,box有就放,然后给下一位
没有那就找一个最近的1,然后下一次循环就从这个最近的1开始就行了
其实想清楚了就蛮简单的,但是当时没想到。还是多做题吧
这是一种思路,写一个循环,当前的信息一直传递给后面
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
int bag[70], box[70], m;
ll n;
int f(int x)
{
int res = 0;
for(; x; x >>= 1) res++;
return res;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
memset(bag, 0, sizeof(bag));
memset(box, 0, sizeof(box));
scanf("%lld%d", &n, &m);
int pos = 0;
for(ll t = n; t; t >>= 1)
bag[++pos] = t & 1;
ll sum = 0;
while(m--)
{
int x; scanf("%d", &x);
sum += x;
box[f(x)]++;
}
if(sum <= n)
{
puts(sum < n ? "-1" : "0");
continue;
}
int ans = 0;
_for(i, 1, pos)
{
if(bag[i] == 0) box[i + 1] += box[i] / 2;
else
{
if(box[i]) box[i]--, box[i + 1] += box[i] / 2;
else
{
int t = i + 1;
while(!box[t]) t++;
ans += t - i;
box[t]--;
i = t - 1;
}
}
}
printf("%d\n", ans);
}
return 0;
}
周日 4.18(训练赛补题)
昨天队内打了一场队内训练赛,还行吧。认真补题,补每一道题
1.vjudge网站问题下次要解决,可以带个笔记本电脑过来
2.不要死磕一道题。比赛时我死磕一道题,以为自己思路很正确,就是差一点优化。但是后来看了题解发现自己思路是错误的。
所以不要在一道题上花太多时间
P1972 [SDOI2009]HH的项链(离线 + 树状数组)
由于前段时间一直在做线段树,区间染色问题等等
这道题一读完题马上开打线段树,然后超时了
我还纠结为什么超时,不用线段树用什么
现在看来,用线段树是会超时的,这个思路是错的
现在来看看正解
这题和之前的线段树题有一点不同,就是没有修改,是静态的
也就是说可以离线
首先一段区间里面不同颜色的个数,就是区间长度减去重复的个数
那么问题的关键在于如果算出重复的个数
一段区间1 2 3 1
这里重复了一个1,显然要删掉一个1
那么按照正常的逻辑从小到大,我们删掉左边那个1
所以重复的元素就用最右边那个元素代表
那么一些区间,如果它们右端点一样的话,那么每个元素最右端的位置是一样的
所以我们按照区间右端点排序
用树状数组来维护这个位置的元素是否存在,因为重复就要删掉
用pre数组维护这个元素重复的前一个位置,每次先删掉之前的重复元素再加当前的元素
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int f[N], a[N], pre[N], n, m;
struct segment
{
int l, r, id, val;
}s[N];
int lowbit(int x) { return x & (-x); }
void add(int x, int p)
{
for(; x < N; x += lowbit(x))
f[x] += p;
}
int sum(int x)
{
int res = 0;
for(; x; x -= lowbit(x))
res += f[x];
return res;
}
bool cmp1(segment a, segment b)
{
return a.r < b.r;
}
bool cmp2(segment a, segment b)
{
return a.id < b.id;
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
scanf("%d", &m);
_for(i, 1, m)
{
int l, r;
scanf("%d%d", &l, &r);
s[i] = {l, r, i, 0};
}
sort(s + 1, s + m + 1, cmp1);
int r = 0;
_for(i, 1, m)
{
if(s[i].r > r)
{
_for(j, r + 1, s[i].r)
{
if(pre[a[j]]) add(pre[a[j]], -1);
pre[a[j]] = j;
add(j, 1);
}
r = s[i].r;
}
s[i].val = sum(s[i].r) - sum(s[i].l - 1);
}
sort(s + 1, s + m + 1, cmp2);
_for(i, 1, m) printf("%d\n", s[i].val);
return 0;
}
K. King's Task(思维)
这道cf1200的题竟然没做出来
原因是想复杂了
我一直在想怎么推出来答案
其实直接暴力枚举
因为两次同样操作就还原了,所以操作要交替进行
所以暴力枚举就好了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int a[N << 1], t[N << 1], n;
int work(int op)
{
int res = 0;
_for(i, 1, 2 * n) t[i] = a[i];
while(1)
{
int ok = 1;
_for(i, 1, 2 * n)
if(t[i] != i)
{
ok = 0;
break;
}
if(ok) return res;
res++;
if(op) for(int i = 1; i <= 2 * n; i += 2) swap(t[i], t[i + 1]);
else _for(i, 1, n) swap(t[i], t[n + i]);
op = op ^ 1;
ok = 1;
_for(i, 1, 2 * n)
if(t[i] != a[i])
{
ok = 0;
break;
}
if(ok) return 1e9;
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, 2 * n) scanf("%d", &a[i]);
int ans = min(work(0), work(1));
printf("%d\n", ans == 1e9 ? -1 : ans);
return 0;
}
下周就是认真补题,以及学习题目中暴露出来的缺失的知识点,学一些高频知识点
如莫队,扫描线,主席树,简单dp,计算几何等等
那各种dp不着急
H. K and Medians(思维)
这是一道cf2200的思维题,我没做出来
首先一个很明显的就是一定要(n - m) mod (k - 1) = 0才可以
然后我们考虑最后一次删除,在某一个b位置,前面删掉(k - 1)/2, 后面删掉(k - 1)/2
也就是说无论如何一定要存在某个b,前面至少有(k - 1)/2个要删掉的,后面至少有(k - 1) / 2要删掉的
那么我们接下来考虑,多出来的部分怎么办
我们可以一直删,直到前面剩下(k-1)/2,后面也是
如果前面多的部分多就前面删的1数多一些,后面同理
因为(n - m) mod (k - 1) = 0,所以从总数上来说是可以实现的
具体怎么删也是有正确的策略的,其实不必太去深究,相当于猜结论
总结一下,这道思维题的难度在于直接从最后一步考虑,从最后一步推出条件,然后猜满足这条件整个就可以成立
有些思维题就是要大胆猜结论
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
int a[N], s1[N], s2[N], n, k, m;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d%d%d", &n, &k, &m);
_for(i, 1, n) a[i] = 0;
_for(i, 1, m)
{
int x; scanf("%d", &x);
a[x] = 1;
}
if((n - m) % (k - 1) != 0)
{
puts("NO");
continue;
}
s2[n + 1] = s1[0] = 0;
_for(i, 1, n) s1[i] = s1[i - 1] + (a[i] == 0);
for(int i = n; i >= 1; i--) s2[i] = s2[i + 1] + (a[i] == 0);
int ok = 0;
_for(i, 1, n)
if(a[i] && s1[i - 1] >= (k - 1) / 2 && s2[i + 1] >= (k - 1) / 2)
{
ok = 1;
break;
}
puts(ok ? "YES" : "NO");
}
return 0;
}
博主本周专注于割点和桥相关算法题训练,包括边双联通分量缩点、点双联通分量等题目,还做了CF杂题和训练赛补题。如在求点双联通分量时有不同方法,做思维题需大胆猜结论等,下周计划补题并学习缺失知识点。
7965

被折叠的 条评论
为什么被折叠?



