1002:Blow up the city(支配树)
反向拓扑建立支配树,可以说是学习拓扑序的支配树的模板题吧。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e5 + 50;
struct node{
int v, nxt;
}e[maxn*2];
int cnt = 0;
int head[maxn];
void add(int u, int v){
e[cnt].v = v;
e[cnt].nxt = head[u];
head[u] = cnt++;
}
int du[maxn];
int f[maxn][20];
int n, m, rt;
vector<int> fa[maxn];
void init()
{
scanf("%d%d", &n, &m);
memset(head, -1, (n+1)<<2);
memset(du, 0, (n+1)<<2);
for(int i = 1; i <= n; ++i) fa[i].clear();
rt = cnt = 0;
while(m--){//逆拓扑序建图
int u, v;
scanf("%d%d", &v, &u);
add(u, v); du[v]++;
fa[v].push_back(u);//记录父亲
}
for(int i = 1; i <= n; ++i) if(!du[i]) {//建一个总根
add(rt, i); du[i]++;
fa[i].push_back(rt);
}
}
queue<int> q;
int dep[maxn];
int lca(int u, int v){//求lca
if(dep[u] > dep[v]) swap(u, v);
int d = dep[v] - dep[u];
for(int i = 19; i >= 0; --i) if(d&(1<<i)) v = f[v][i];
if(u == v) return v;
for(int i = 19; i >= 0; --i){
if(f[u][i] == f[v][i]) continue;
u = f[u][i], v = f[v][i];
}return f[v][0];
}
void sol(){
while(q.size()) q.pop();
q.push(rt);dep[rt] = 0;//根结点入队
while(q.size()){
int u = q.front(); q.pop();
for(int i = head[u]; i!=-1; i = e[i].nxt){
int v = e[i].v;
du[v]--;
if(!du[v]) q.push(v);
}
if(u == rt) continue;//根节点不用父亲
int p = fa[u][0];
for(int i = 1; i < fa[u].size(); ++i) p = lca(p, fa[u][i]);//u结点在支配树上的父亲为它所有原父亲的lca
dep[u] = dep[p] + 1;
f[u][0] = p;
for(int i = 0; i < 19; ++i) f[u][i+1] = f[f[u][i]][i];
}
scanf("%d", &m);
while(m--){
int u, v;
scanf("%d%d", &u, &v);
int p = lca(u, v);
printf("%d\n", dep[u] + dep[v] - dep[p]);
}
}
int main()
{
int T;cin>>T;
while(T--){
init();sol();
}
}
1008:Game(带修改莫队)
一个数列,两种操作:
1.询问区间[L,R]有多少个异或和不为0的子区间
2.交换
a
p
a_p
ap和
a
p
+
1
a_{p+1}
ap+1的位置
查询异或和不为0的子区间,相当于查询异或和为0的子区间个数,再拿总的子区间减去这个数。
可以统计异或前缀和,如果一个区间[L-1,R]中异或前缀和为x的数有t个,则每两个可以凑一个异或和为0的区间,贡献为t*(t-1)/2。问题可以转化成求区间每种数字的个数。莫队算法可以轻松实现这一点。
考虑操作2,由于只与后面相邻的位置换,所以相当于是修改了这两个点的前缀异或和,其他位置都不变,相当于单点修改。
在普通莫队的基础上加上一个时间的纬度,实现可修改莫队算法。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e5 + 50;
int a[maxn], s[maxn], ha[maxn*20];
ll ans = 0, Ans[maxn];
struct node{//查询
int l, r, id;
int blockl, blockr;
node(){};
node(int _l, int _r, int _id):l(_l), r(_r), id(_id){blockl = l/2000, blockr = r/2000;}
bool operator < (const node& x)const{
if(blockl != x.blockl) return blockl < x.blockl;
if(blockr != x.blockr) return blockr < x.blockr;
return id < x.id;
}
}e[maxn];
int cnt , num;
int id[maxn], pos[maxn];
void add(int x){
ans = ans + ha[x], ha[x]++;
}
void del(int x){
ha[x]--, ans = ans - ha[x];
}
void The_World(int Dio, int l, int r){
int p = pos[Dio];
if(p >= l && p <= r) del(s[p]), add(s[p+1] ^ a[p]);
if(p+1 >= l && p+1 <= r) del(s[p+1]), add(s[p] ^ a[p+1]);
s[p] ^= a[p+1]; s[p+1] ^= a[p];
swap(a[p], a[p+1]);
swap(s[p], s[p+1]);
}
int n, m;
void init(){
int ex = 1;
for(int i = 1; i <= n; ++i) scanf("%d", &a[i]), s[i] = s[i-1] ^ a[i], ex = max(ex, a[i]);
memset(ha, 0, (2*ex+1)<<2);
memset(Ans, -1, (m+1)<<3);
num = cnt = 0;
ans = 0;
}
void sol()
{
for(int i = 1; i <= m; ++i){
int op;scanf("%d", &op);
if(op == 1){
int l, r;
scanf("%d%d", &l, &r);
e[cnt++] = node(l-1, r, i);
}
else{
id[++num] = i;//
scanf("%d", &pos[num]);
}
}
id[++num] = m + 1;
sort(e, e+cnt);
int cur = 0;//当前时间
int lp = 1, rp = 1;//当前左右端点
add(s[1]);
for(int i = 0; i < cnt; ++i){
int l = e[i].l, r = e[i].r;
while(id[cur+1] < e[i].id) The_World(++cur, lp, rp);//时间啊,流动吧!
while(id[cur] > e[i].id) The_World(cur--, lp, rp);//你永远无法抵达“真实”!
while(rp < r) add(s[++rp]);
while(lp > l) add(s[--lp]);
while(rp > r) del(s[rp--]);
while(lp < l) del(s[lp++]);
ll t = r-l+1;
t = t*(t-1)/2;
Ans[e[i].id] = t-ans;
}
for(int i = 1; i <= m; ++i){
if(Ans[i] != -1) printf("%lld\n", Ans[i]);
}
}
int main()
{
while(scanf("%d%d", &n, &m)!=EOF){
init();sol();
}
}
1009:K Subsequence(费用流)
选最多k个不相交单调上升子序列使得总和最大。相当于一个流量限制为k的费用流模型。对于每个数a[i]拆成入点和出点,自己的入点到出点流量为1,费用为-a[i],流过这条边相当于选择了这个点。然后这个点的出点向后面所有比它大的点的入点连流量为1,费用为0的边。每个点的出点向汇点连边,如果走了这条边说明以该点为结尾。由于要限制流量k,所以可以源点向一个额外的t点连流量为k的边,这个t点再向所有入点连边,走这条边表明选择这个点作为开始。
spfa的费用流被卡,喜提dijstra优化的费用流板子
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int>P;//first保存最短距离,second保存顶点的编号
const int maxn=1e4+50;
const int INF=0x3f3f3f3f;
int read(){
char c = getchar();
int x = 0;
while(c < '0' || c > '9') c = getchar();
while(c >= '0' && c <= '9') x = x*10 + c - '0', c = getchar() ;
return x;
}
struct Edge
{
int to, cap, cost, rev;//终点,容量(指残量网络中的),费用,反向边编号
Edge(int t, int c, int cc, int r) :to(t), cap(c), cost(cc), rev(r) {}
};
struct MCMF
{
int V;//顶点数
vector<Edge>G[maxn];//图的邻接表
int h[maxn];//上一次顶点的最短路s到v的最短路径
int dist[maxn];//最短距离
int prevv[maxn];//最短路中的父结点
int preve[maxn];//最短路中的父边
void init(int n)
{
for(int i=0; i<=n; ++i) G[i].clear();
V = n;
}
void add(int from, int to, int cap, int cost)
{
G[from].push_back(Edge( to, cap, cost, G[to].size()));
G[to].push_back(Edge( from, 0, -cost, G[from].size() - 1 ));
}
int min_cost_flow(int s, int t, int f)//返满足流f的最小费用 不能满足返回-1
{
int res = 0;
fill(h, h + V + 1, 0);//0~V清空
while (f>0)//f>0时还需要继续增广
{
priority_queue<P, vector<P>, greater<P> >q;
fill(dist, dist + V + 1, INF);//距离初始化为INF
dist[s] = 0;
q.push(P(0, s));
while (!q.empty())
{
P p = q.top();
q.pop();
int v = p.second;
if (dist[v]<p.first) continue;//p.first是v入队列时候的值,dist[v]是目前的值,如果目前的更优,扔掉旧值
for (int i = 0; i<G[v].size(); i++)
{
Edge &e = G[v][i];
if (e.cap>0 && dist[e.to]>dist[v] + e.cost + h[v] - h[e.to])//松弛操作
{
dist[e.to] = dist[v] + e.cost + h[v] - h[e.to];
prevv[e.to] = v;//更新父结点
preve[e.to] = i;//更新父边编号
q.push(P(dist[e.to], e.to));
}
}
}
if (dist[t] == INF)//如果dist[t]还是初始时候的INF,那么说明s-t不连通,不能再增广了
break;//
for (int i = 0; i<= V; i++)//更新h
h[i] += dist[i];
int d = f;
int sum=0;
for (int x = t; x != s; x = prevv[x]){
d = min(d, G[prevv[x]][preve[x]].cap);//从t出发沿着最短路返回s找可改进量
sum+=G[prevv[x]][preve[x]].cost;
}
f -= d;
res += d*sum;//h[t]表示最短距离的同时,也代表了这条最短路上的费用之和,乘以流量d即可得到本次增广所需的费用
for (int x = t; x != s; x = prevv[x])
{
Edge&e = G[prevv[x]][preve[x]];
e.cap -= d;//修改残量值
G[x][e.rev].cap += d;
}
}
return res;
}
}mvmf;
int n, k;
int st, ed;
int a[maxn];
void init(){//这部分是连边操作,其他都是板子
n = read();
k = read();
st = 0, ed = 2*n+1;
int tt = 2*n + 2;
mvmf.init(tt);
mvmf.add(st, tt, k, 0);
for(int i = 1; i <= n; ++i) {
a[i] = read();
mvmf.add(tt, i, 1, 0);
mvmf.add(i, i+n, 1, -a[i]);
mvmf.add(i+n, ed, 1, 0);
}
for(int i = 1; i <= n; ++i){
for(int j = i+1; j <= n; ++j){
if(a[j] < a[i]) continue;
mvmf.add(i+n, j, 1, 0);
}
}
}
int main()
{
int T;cin>>T;
while(T--){
init();
printf("%d\n", -mvmf.min_cost_flow(st, ed, INF));
}
}
1011:Squrirrel(树形DP)
题意:
给你一棵树,可以选择一个点然后删除一条边,问删除边后点到叶子节点的最长距离的最小值。
如果没有删边操作,这就是一个树形DP的经典例题。
考虑删边操作,其实也差不多,但是细节比较繁琐。
计
d
i
s
[
u
]
[
0
/
1
/
2
]
dis[u][0/1/2]
dis[u][0/1/2]分别为以u为根的结点向下的最长,次长,第三长的距离,
d
i
s
[
u
]
[
3
]
dis[u][3]
dis[u][3]为u结点向父亲方向走的最长距离。这部分不用考虑删边。
设
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]为以u为根节点,在下面删除一条边之后,向下走的最长距离的最小值。
设
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]为在u向父亲方向走的部分删除一条边之后,u向父亲方向走的最长距离的最小值。
有了这些之后就可以开始dp了,细节看代码吧。
#include<bits/stdc++.h>
#define ll long long
#define P pair<int, int>
using namespace std;
const int maxn = 2e5 + 50;
const int inf = 0x3f3f3f3f;
int dis[maxn][4], dp[maxn][2], fa[maxn], we[maxn];
vector<P> g[maxn];
int n;
void init()
{
scanf("%d", &n);
for(int i = 0; i <= n; ++i) g[i].clear();
for(int i = 1; i < n; ++i){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u].push_back(P(v, w));
g[v].push_back(P(u, w));
}
}
void dfs1(int u, int f, int d)//获取fa,we,dis012,dp0
{
fa[u] = f;
we[u] = d;
dis[u][0] = dis[u][1] = dis[u][2] = 0;
for(int i = 0; i < g[u].size(); ++i){
int v = g[u][i].first, w = g[u][i].second;
if(v == f) continue;
dfs1(v, u, w);
int t = dis[v][0] + w;
if(t > dis[u][0]) dis[u][2] = dis[u][1], dis[u][1] = dis[u][0], dis[u][0] = t;
else if(t > dis[u][1]) dis[u][2] = dis[u][1], dis[u][1] = t;
else if(t > dis[u][2]) dis[u][2] = t;
}
dp[u][0] = 0;//dp[u][0]表示删除一条边后向下的最长距离的最小值
for(int i = 0; i < g[u].size(); ++i){
int v = g[u][i].first, w = g[u][i].second;
if(v == f) continue;
if(dis[u][0] == dis[v][0] + we[v]) {//这个状态一定是去删最长路中的边
dp[u][0] = max(dis[u][1], min(dis[v][0], we[v] + dp[v][0]));//删直接边或者删子树中的边
break;
}
}
}
int ans[maxn];
void dfs2(int u, int f)//获取dis3,dp1
{
if(dis[u][0] + we[u] == dis[f][0]){//它是父亲的最长分支
dis[u][3] = we[u] + max(dis[f][3], dis[f][1]);
}
else dis[u][3] = we[u] + max(dis[f][3], dis[f][0]);
int t1, t2;//t1删直接边,t2删其他边
if(dis[u][0] + we[u] == dis[f][0]) t1 = max(dis[f][3], dis[f][1]);//它是最长路分支
else t1 = max(dis[f][3], dis[f][0]);//它不是
if(dis[u][0] + we[u] == dis[f][0]){//它是父亲向下最长路分支
int w = 0;
int v = 0;
for(int i = 0; i < g[f].size(); ++i){
v = g[f][i].first;
if(v == u || v == fa[f]) continue;
if(we[v] + dis[v][0] == dis[f][1]){
w = we[v];
break;
}
}
int temp1 = dis[f][1] - w;//删除直接相连的边
int temp2 = w + dp[v][0];//删除v下面的边
w = max(dis[f][2], min(temp1, temp2));//两种删法取最小,和第三条支线取最大
t2 = we[u] + min( max(dp[f][1], dis[f][1]), max(dis[f][3], w) );//直接边 + 两种删法较小的
}
else if(dis[u][0] + we[u] == dis[f][1]){//它是次大的
int v = 0;
for(int i = 0; i < g[f].size(); ++i){//找到最大的儿子
v = g[f][i].first;
if(v == u || v == fa[f]) continue;
if(we[v] + dis[v][0] == dis[f][0]) break;
}
int temp1 = max(dis[f][3], min(we[v] + dp[v][0], dis[v][0]));//向下删
int temp2 = max(dp[f][1], dis[f][0]);
t2 = we[u] + min(temp1, temp2);
}
else{//它不是最长也不是次长
t2 = we[u] + min( max(dp[f][1], dis[f][0]), max(dis[f][3], dp[f][0]) );//直接边 + 两种删法中较小的
}
dp[u][1] = min(t1, t2);
ans[u] = min( max(dp[u][1], dis[u][0]), max(dp[u][0], dis[u][3]) );
for(int i = 0; i < g[u].size(); ++i){
int v = g[u][i].first;
if(v == f) continue;
dfs2(v, u);
}
}
void sol()
{
dfs1(1, 0, 0);
dfs2(1, 0);
int k = 1;
for(int i = 2; i <= n; ++i) {
if(ans[i] < ans[k]) k = i;
}printf("%d %d\n", k, ans[k]);
// for(int i = 6; i <= 6; ++i){
// cout<<"i:"<<i<<" dis0:"<<dis[i][0]<<" dis1:"<<dis[i][1]<<" dis2:"<<dis[i][2];
// cout<<" dis3:"<<dis[i][3]<<" dp0:"<<dp[i][0]<<" dp1:"<<dp[i][1]<<" ans:"<<ans[i]<<endl;
// }
}
int main()
{
int T;cin>>T;
while(T--){
init();sol();
}
}