2019杭电第三场补题

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();
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值