【动态规划】loj#2485. 「CEOI2017」Chase
有意思的可做dp题;细节有点多,值得多想想
题目描述
在逃亡者的面前有一个迷宫,这个迷宫由 nnn 个房间和 n−1n-1n−1 条双向走廊构成,每条走廊会链接不同的两个房间,所有的房间都可以通过走廊互相到达。换句话说,这是一棵树。
逃亡者会选择一个房间进入迷宫,走过若干条走廊并走出迷宫,但他永远不会走重复的走廊。
在第 iii 个房间里,有 FiF_iFi 个铁球,每当一个人经过这个房间时,他就会受到铁球的阻挡。逃亡者手里有VVV个磁铁,当他到达一个房间时,他可以选择丢下一个磁铁(也可以不丢),将与这个房间相邻的所有房间里的铁球吸引到这个房间。这个过程如下:
- 逃亡者进入房间。
- 逃亡者丢下磁铁。
- 逃亡者走出房间。
- 铁球被吸引到这个房间。
注意逃亡者只会受到这个房间原有的铁球的阻拦,而不会受到被吸引的铁球的阻挡。
在逃亡者走出迷宫后,追逐者将会沿着逃亡者走过的路径穿过迷宫,他会碰到这条路径上所有的铁球。
请帮助逃亡者选择一条路径,使得追逐者遇到的铁球数量减去逃亡者遇到的铁球数量最大化。
输入格式
第一行两个空格隔开的整数整数 nnn 和 VVV。
第二行 nnn 个空格隔开的整数表示 FiF_iFi。
之后的 n−1n-1n−1 行,每行两个空格隔开的整数 xxx 和 yyy,表示有一条走廊连接编号为 xxx 和编号为 yyy 的房间。
输出格式
输出一个整数表示最优情况下追逐者遇到的铁球数量减去逃亡者遇到的铁球数量。
样例
样例输入
12 2
2 3 3 8 1 5 6 7 8 3 5 4
2 1
2 7
3 4
4 7
7 6
5 6
6 8
6 9
7 10
10 11
10 12
样例输出
36
样例解释
有一个最优方案如下:
- 从 6 号房间进入迷宫并丢下第一个磁铁,他遇到了 5 个铁球,这个时候 6 号房间会有 27 个铁球,而 5 号,7 号,8 号,9 号房间都没有铁球。
- 走到 7 号房间丢下第二个磁铁并走出迷宫,他遇到了 0 个铁球,这个时候 7 号房间会有 41 个铁球,而 2 号,4 号,6 号,10 号房间会没有铁球。
在这个过程中,逃亡者会遇到 5 个铁球而追逐者会遇到 41 个铁球。
数据范围与提示
对于 100% 的数据,有 1≤n≤105;0≤V≤100;0≤Fi≤109
- 子任务 1(20%): 有 1≤n≤10
- 子任务 2(20%): 有 1≤n≤1000
- 子任务 3(30%): 保证存在一条从 1 号房间开始的最优路径;
- 子任务 4(30%): 无特殊限制。
题目分析
搜索
做法分析:
时间复杂度$O(n^22^n)$;期望得分20pts。
贪心
考虑若固定一条路径,应该怎样选取路径上的点使得答案最优。
首先追逐者遇到的铁球可以分成两部分:所有原先路径上的铁球;被吸引到路径上的铁球。而逃亡者遇到的铁球则是所有原先路径上的铁球,减去被吸引的铁球。
也就是说,两者遇到的铁球的差可以分成两部分:逃亡者避开的铁球+逃亡者从其他路径吸引来的铁球。
那么问题转化为:对于一颗树,定义一条路径的价值为路径上前$v$大的点权之和,要求一条以根为起点的最大价值路径。
这个东西相当于要求支持操作:查询前k大元素和;加入一个元素;删除任意一个元素。
用平衡树当然也是可以的,不过有一种堆的做法。
(最早做的时候,想到了贪心这步但是卡在查询路径前$k$大元素和这步了……一直在想可持久化堆?之类的奇怪东西)
众所周知删除堆有两种非常普遍的方法:1.强制弹出直到堆顶元素为删除元素,之后再依次弹回;2.在堆外标记元素被删除。第二种方法在权值小的时候很通用,不过权值一大且没法离散化时候就不行了。第三种是设置一个“删除堆”,存要删除的元素,当删除堆的堆顶和现在堆顶相同就舍弃现在堆顶。
于是就可以愉快地贪心了。
#include<bits/stdc++.h>
const int maxn = ; int n,v;
int p[maxn];
long long ans,sv[],sum;
bool vis[maxn];
std::priority_queue<long long> q,del;
int edgeTot,edges[maxn<<],nxt[maxn<<],head[maxn]; int read()
{
char ch = getchar();
int num = ;
bool fl = ;
for (; !isdigit(ch); ch = getchar())
if (ch=='-') fl = ;
for (; isdigit(ch); ch = getchar())
num = (num<<)+(num<<)+ch-;
if (fl) num = -num;
return num;
}
void addedge(int u, int v)
{
edges[++edgeTot] = v, nxt[edgeTot] = head[u], head[u] = edgeTot;
edges[++edgeTot] = u, nxt[edgeTot] = head[v], head[v] = edgeTot;
}
void clears(std::priority_queue<long long> &q)
{
std::priority_queue<long long> emt;
std::swap(q, emt);
}
void dfs(int x, int fa)
{
long long res = , cnt = ;
int tot = ;
for (int i=head[x]; i!=-; i=nxt[i])
if (!vis[edges[i]]) cnt += p[edges[i]];
q.push(cnt), vis[x] = , sum += cnt;
for (int i=head[x]; i!=-; i=nxt[i])
{
int to = edges[i];
if (!vis[to]){
dfs(to, x);
}
}
if (sum > ans){
while (tot<v&&q.size())
{
while (del.size()&&del.top()==q.top())
q.pop(), del.pop();
if (q.empty()) break;
sv[++tot] = q.top(), res += q.top();
q.pop();
}
ans = ans > res?ans:res;
for (int i=; i<=tot; i++) q.push(sv[i]);
}
sum -= cnt, vis[x] = , del.push(cnt);
}
int main()
{
memset(head, -, sizeof head);
n = read(), v = read();
for (int i=; i<=n; i++) p[i] = read();
for (int i=; i<n; i++) addedge(read(), read());
if (n <= )
for (int i=; i<=n; i++)
clears(q), clears(del), dfs(i, i);
else dfs(, );
printf("%lld\n",ans);
return ;
}
做法分析:
$n$次枚举起点,每次枚举起点后$n$次枚举终点。对于每一条路径$vlogv$更新答案。
由于用了优先队列,常数略大,需要卡常或者如上剪枝。
时间复杂度$O(n^2vlogv)$;期望得分70pts。
浅层的动态规划
枚举树根,令$f[i][j]$表示以$i$为根的子树内,以$i$为起点,选取$j$个点的一条链获得的最大价值。
由于枚举树根,因此所有情况都会被包括在内。
做法分析:
$n$次枚举树根,每次dp状态$O(nv)$,转移$O(1)$.
时间复杂度$O(n^2v)$;期望得分70pts。
深入的动态规划
上一个做法的瓶颈在于考虑的是整条路径,因此多次dp中重叠的信息较难合并,只能枚举树根。
事实上可以强制以$1$为根,规定“上”和“下”的顺序。于是就和求树的直径很类似地,合法的路径要么一条上;一条下;两条上下的组合起来。
用$up[x][i]$表示从$x$的子树中某个节点向上走到$x$,总共使用了$i$个节点的最大价值;相同的,$dx[x][i]$表示一条向下路径使用$i$个节点的最大价值。
现在的关键就在于转移时候的去重,不能使两条链有重复部分。
因此update部分是这个样子:
void update(int x, int y, int fa)
{
for (int i=; i<v; i++) getMax(ans, up[x][i]+dw[y][v-i]);
for (int i=; i<=v; i++)
getMax(up[x][i], max(up[y][i], up[y][i-]+sum[x]-p[y])),
getMax(dw[x][i], max(dw[y][i], dw[y][i-]+sum[x]-p[fa]));
}
还有需要注意的一点是,从$s$到$t$的路径价值和从$t$到$s$的价值是不一样的,所以每一次dfs还需要把子节点的顺序反过来再做一遍。
#include<bits/stdc++.h>
typedef long long ll;
const int maxn = ; int n,v;
int p[maxn];
ll ans,up[maxn][],dw[maxn][],sum[maxn];
std::vector<int> g[maxn]; #define BUF_SIZE 100000
#define OUT_SIZE 100000
inline char nc(){
static char buf[BUF_SIZE],*p1=buf+BUF_SIZE,*pend=buf+BUF_SIZE;
if (p1==pend){
p1=buf; pend=buf+fread(buf,,BUF_SIZE,stdin);
if (pend==p1)return -;
}
return *p1++;
}
int read()
{
char ch = nc();
int num = ;
bool fl = ;
for (; !isdigit(ch); ch = nc())
if (ch=='-') fl = ;
for (; isdigit(ch); ch = nc())
num = (num<<)+(num<<)+ch-;
if (fl) num = -num;
return num;
}
void getMax(ll &x, ll y){x=(x>y)?x:y;}
ll max(ll x, ll y){return x>y?x:y;}
void update(int x, int y, int fa)
{
for (int i=; i<v; i++) getMax(ans, up[x][i]+dw[y][v-i]);
for (int i=; i<=v; i++)
getMax(up[x][i], max(up[y][i], up[y][i-]+sum[x]-p[y])),
getMax(dw[x][i], max(dw[y][i], dw[y][i-]+sum[x]-p[fa]));
}
void dfs(int x, int fa)
{
for (int i=; i<=v; i++) up[x][i] = sum[x], dw[x][i] = sum[x]-p[fa];
for (auto &to:g[x])
if (to!=fa)
dfs(to, x), update(x, to, fa);
std::reverse(g[x].begin(), g[x].end());
for (int i=; i<=v; i++) up[x][i] = sum[x], dw[x][i] = sum[x]-p[fa];
for (auto &to:g[x])
if (to!=fa) update(x, to, fa);
getMax(ans, max(up[x][v], dw[x][v]));
}
int main()
{
n = read(), v = read();
for (int i=; i<=n; i++) p[i] = read();
for (int i=; i<n; i++)
{
int x = read(), y = read();
g[x].push_back(y), g[y].push_back(x);
sum[x] += p[y], sum[y] += p[x];
}
dfs(, );
printf("%lld\n",ans);
return ;
}
做法分析:
这里总的状态数是$O(nv)$的。对于$O(n)$个点来说,其每一个子节点的转移是$O(v)$的。每个点只会作为子节点被转移一次,因此复杂度是$O(nv)$的。
时间复杂度$O(nv)$;期望得分100。
END
【动态规划】loj#2485. 「CEOI2017」Chase的更多相关文章
- loj#2483. 「CEOI2017」Building Bridges 斜率优化 cdq分治
loj#2483. 「CEOI2017」Building Bridges 链接 https://loj.ac/problem/2483 思路 \[f[i]=f[j]+(h[i]-h[j])^2+(su ...
- loj#2483. 「CEOI2017」Building Bridges(dp cdq 凸包)
题意 题目链接 Sol \[f[i], f[j] + (h[i] - h[j])^2 + (w[i - 1] - w[j]))\] 然后直接套路斜率优化,发现\(k, x\)都不单调 写个cdq就过了 ...
- 【刷题】LOJ 2480 「CEOI2017」One-Way Streets
题目描述 给定一张 \(n\) 个点 \(m\) 条边的无向图,现在想要把这张图定向. 有 \(p\) 个限制条件,每个条件形如 \((xi,yi)\) ,表示在新的有向图当中,\(x_i\) 要能够 ...
- @loj - 2480@ 「CEOI2017」One-Way Streets
目录 @description@ @solution@ @accepted code@ @details@ @description@ 给定一张 n 个点 m 条边的无向图,现在想要把这张图定向. 有 ...
- @loj - 2483@「CEOI2017」Building Bridges
目录 @desription@ @solution@ @accepted code@ @details@ @another solution@ @another code@ @desription@ ...
- Loj #2192. 「SHOI2014」概率充电器
Loj #2192. 「SHOI2014」概率充电器 题目描述 著名的电子产品品牌 SHOI 刚刚发布了引领世界潮流的下一代电子产品--概率充电器: 「采用全新纳米级加工技术,实现元件与导线能否通电完 ...
- Loj #3096. 「SNOI2019」数论
Loj #3096. 「SNOI2019」数论 题目描述 给出正整数 \(P, Q, T\),大小为 \(n\) 的整数集 \(A\) 和大小为 \(m\) 的整数集 \(B\),请你求出: \[ \ ...
- Loj #3093. 「BJOI2019」光线
Loj #3093. 「BJOI2019」光线 题目描述 当一束光打到一层玻璃上时,有一定比例的光会穿过这层玻璃,一定比例的光会被反射回去,剩下的光被玻璃吸收. 设对于任意 \(x\),有 \(x\t ...
- Loj #3089. 「BJOI2019」奥术神杖
Loj #3089. 「BJOI2019」奥术神杖 题目描述 Bezorath 大陆抵抗地灾军团入侵的战争进入了僵持的阶段,世世代代生活在 Bezorath 这片大陆的精灵们开始寻找远古时代诸神遗留的 ...
随机推荐
- 【BZOJ1174】: [Balkan2007]Toponyms
→原题← ↑这样子我就不复制题面啦~ 就是一题很裸的字典树而已,不过空间卡的很死,直接开个数组 tr[N][52] 什么之类的一定会RE的(惨痛的教训) 当字典树空间不够而时间限制又比较宽松时,我们可 ...
- go系列(5)- beego自己写controller
前边的系列文章已经讲述了如何安装环境, beego的处理逻辑都是在Controller里面完成的,下面就写一个最简单的Controller. 我们在写自己的controller的时候,一定要继承bee ...
- python 基础(九) 文件操作
文件操作 一.函数: f = open(’文件名','打开方式'[,encoding='字符编码']) open 打开的方式 字符 说明 r 只读的方式打开 rb 以二进制的形式打开文件 只读 r+ ...
- HTML图像标记
1.使用方法 <img src="路径/文件名.图片格式" width="属性值" height="属性值" border=" ...
- Unity Shader入门精要学习笔记 - 第2章 渲染流水线
来源作者:candycat http://blog.csdn.net/candycat1992/article/ 2.1 综述 渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏 ...
- ListBox,CheckBoxList,DropDownList,RadioButtonList的常见数据绑定
ListBox,CheckBoxList,DropDownList,RadioButtonList的常见用法 四个都是选择控件,用法大同小异,基本都是指定键值对: 直接加选择项: void way1( ...
- 难道这就是gin中间件的原理,一个装饰者模式而已?
func wrapCtx(handler func(ctx *gin.Context)) gin.HandlerFunc { return func(c *gin.Context) { //获取请求的 ...
- lnmp.org + phpstorm + xdebug
lnmp.org下载安装包安装之: lnmp是个集成安装包,就不用自己在配置lnmp环境 安装phpstorm,破解方法:注册服务器为http://idea.lanyus.com 就可以了 xdebu ...
- JVM垃圾回收机制一
JVM内存分配与回收 JVM 分代 JVM把堆分为年轻代和老年代,年轻代又分为1个Eden区和2个Survivor区,Eden和Survivor的内存的大小比例是8:1:1. 为什么要分代? 很大的原 ...
- PHP识别二维码功能,php-zbarcode 安装
php-zbarcode是PHP识别二维码的扩展. 下面是安装方法,安装前要先安装ImageMagick.zbar. php-zbarcode 下载地址 安装ImageMagick: yum inst ...