首页 > 其他分享 >[HAOI2012] 高速公路 题解

[HAOI2012] 高速公路 题解

时间:2023-08-28 23:24:32浏览次数:44  
标签:lazy int 题解 rson 收费站 高速公路 lson HAOI2012 id

[HAOI2012] 高速公路 题解

题目链接

题目要求我们求期望,先考虑一下求期望的公式。

根据期望的定义得:期望费用 \(E_v = \dfrac{所有可能路线的总费用}{所有可能路线的数量}\).

其中,所有可能路线的数量 \(= C_{R-L+1}^2 = (R-L+1)(R-L)\),可以在常数时间内计算。(这里用大写的 \(L\),\(R\) 代替题目中每次询问和修改操作中给出的 \(l\),\(r\).)在每次询问中,\(R\),\(L\) 都是不变的。所以对于每次询问,在这个值本身可以当作一个常数,而无需多次计算。下面,主要讨论所有可能路线的总费用的计算。

一般的思维方式是,从路线出发,遍历所有可能的起点与终点,对于每一组起点与终点所确定的路线,计算它的花费,并累加到总花费中。但这样的时间复杂度达到了 \(O(n^2)\) (对于每次询问),而且不好优化。

换一种思路,从每一个路段出发,直接统计它在所有可能的路线中被收费了多少次,再乘上它的花费,就是这个路段对总费用的贡献,即:\(所有可能路线的总费用 v_总 = \sum \limits _{L \le i < R} v_i \times i 收费站收费的次数\).

(注意这个式子中,是“收费站”而不是“路段”,这与题目中描述的不同,题面说的是经过某一条路段会收费,而不是在收费站收费。这里为了方便,把边权转化为点权,即每一条路段的费用转化为这一条路段起点收费站的费用。例如把 \(1 \sim 2\) 收费站之间路段的费用转化为 \(1\) 收费站的费用。这样改变之后,对于一整条路线的费用计算也要相应变化:以 \(l\) 收费站为起点,\(r\) 收费站为终点的路线,其总费用为 \(l\) 到 \(r-1\) 号收费站的费用之和。需要注意的是,对于反方向的行驶,即 \(r < l\) 的情况,费用的计算是相同的,因为题目中说明从 \(i\) 收费站行驶到 \(i+1\) 收费站和从 \(i+1\) 收费站行驶到 \(i\) 收费站的花费是相同的——在化边权为点权之后,这个费用就是 \(i\) 收费站的费用。下面再讨论时,便不再特殊考虑反方向行驶的情况。)

(当然,不把边权化为点权也是可行的,本质上没有区别,只是在考虑边界情况的时候有点不同。这里这样做只是为了方便。)

那么,如何计算 \(i\) 收费站收费的次数呢?设一条路线的起点为 \(l\),终点为 \(r\),那么这条路线上收费的收费站应该是 \(l \sim r-1\) 号收费站:注意,因为化边权为点权,所以最后一个收费站并不收费(这也是为什么我说的是“收费的次数”而不是“被经过”的次数)。反过来,对于一个收费站 \(i\) 进行了收费,那么这条路段的起点小于等于 \(i\) 而终点大于 \(i\) (再次强调是大于而不是大于等于),即 \(l \le i < r\). 在 \(L\) 和 \(R\) 都确定的情况下,对于收费站 \(i\),在被它收费的所有路段中,可以作为起点的收费站有 \((i-L+1)\) 个,可以作为终点的收费站有 \((R-i)\) 个(根据化边权为点权的思想,收费站自己作为起点的时候,这条路段会被它收费,作为终点是则不会,所以是 \((R-i)\) 而不是 \((R-i+1)\))。根据乘法原理,这些起点和终点所确定的路线有 \((i-L+1)(R-i)\) 条,所以 \(i\) 收费站对总费用的贡献为 \(v_i \times (i-L+1)(R-i)\).

这样一来,每次询问所需的事件复杂度为 \(O(n)\):遍历 \(L \sim R\) 之间所有的收费站时 \(O(n)\) 的,而对于每一个收费站计算它的贡献是常数时间。但是 \(n\) 和操作次数都为 \(10^5\) 量级,这个时间复杂度仍然不够优。而看到区间修改和区间查询,我们很自然的想到了线段树。

线段树里面需要维护什么?重新审视这个问题,题目中的修改操作不过是简单的区间加,无需多言;而查询操作,根据上文的推导,是在求 \(\sum_{L \le i<R} v_i \times (i-L+1)(R-i)\). 这种形式并不好用线段树查询,因为式子中含有 \(L\),\(R\) 这两个只在询问中给出的变量,我们应该把这个东西提出到求和符号之外,否则无法用线段树维护。下面开始对式子变形:

\[\begin{aligned} 所有可能路线的总费用 v_总 & = \sum \limits _{l \le i < r} v_i \times i 收费站收费的次数 \\ & = \sum \limits _{l \le i < r} v_i \times (i-l+1)(r-i) \\ & = \sum \limits _{l \le i < r} (ir - i^2 -lr + il + r - i)v_i \\ & = \sum \limits _{l \le i < r} -i^{2}v_i + (l+r-1)iv_i - (r-lr)v_i \\ & = -[\sum \limits _{l \le i < r} i^{2}v_i] + [(l+r-1) \sum \limits _{l \le i < r}iv_i] - [(r-lr)\sum \limits _{l \le i < r}v_i] \end{aligned} \]

这样就成功把 \(L\),\(R\) 提到了求和符号之外,而求和符号里面的内容有三项:\(v_i\),\(iv_i\),\(i^2v_i\),下面分别用 sv,svi,svii 表示。 显然,第一项就是区间和,那么第二项和第三项怎么维护呢?从线段树的几个函数考虑:

  1. 合并 update:直接把父节点的值更新为左右子节点的值之和,即
void update(int id)
{
	t[id].sv = t[lson].sv + t[rson].sv;
	t[id].svi = t[lson].svi + t[rson].svi;
	t[id].svii = t[lson].svii + t[rson].svii;
}
  1. 下放 pushdown: 进行区间加的时候,如何用更新这三个值?\(sv\) 不用再讨论,先考虑 \(svi\):已知 \(svi_原 = \sum iv_i\),\(svi_现 = \sum i(v_i+c)\),要把 \(svi_原\) 修改成 \(svi_现\),需要添加的值即为二者的差值,即 \(svi_现 - svi_原 = \sum iv_i - i(v_i + c) = \sum ic = c \sum i\). 事实上这与 sv 的修改没有本质区别,只是加上的 \(c\) 系数有所不同。对于这个 \(\sum i\),这不就是前缀和吗?因为 \(i\) 是连续自然数,所以只要在程序开始先计算一下就可以。同理,对于 \(svii\),得到 \(svii_现 - svii_原 = \sum i^2v_i - i^2(v_i + c) = \sum i^2c = c \sum i^2\),其中的 \(\sum i^2\) 也可以用前缀和解决。代码如下:(为了便于观看,把求前缀和和求区间长度写到了 pushdown 外面。)
ll getlen(int id) {return t[id].right - t[id].left + 1;}
ll getsi(int id) {return si[t[id].right] - si[t[id].left - 1];}
ll getsii(int id) {return sii[t[id].right] - sii[t[id].left - 1];} // si,sii 是前缀和
void pushdown(int id)
{
	if(t[id].lazy)
	{
		t[lson].sv += t[id].lazy * getlen(lson), t[rson].sv += t[id].lazy * getlen(rson);
		t[lson].svi += t[id].lazy * getsi(lson), t[rson].svi += t[id].lazy * getsi(rson);
		t[lson].svii += t[id].lazy * getsii(lson), t[rson].svii += t[id].lazy * getsii(rson);
		t[lson].lazy += t[id].lazy, t[rson].lazy += t[id].lazy;
		t[id].lazy = 0;
	}
}

以上两个函数就是线段树维护的核心了,changequery 并没有什么特殊的地方,这里便掠过。

总结一下,对于每次求和,用 query 函数求出 \(\sum_{L \le i < R} v_i\),\(\sum_{L \le i < R} iv_i\),\(\sum_{L \le i < R} i^2v_i\) ,代入式子计算出所有可能路线的总费用;再 \(O(1)\) 计算出方案数量,然后把二者代入 \(E_v = \dfrac{所有可能路线的总费用}{所有可能路线的数量}\) 中计算出最终的答案。以及,别忘了约分。

至此,这道题的主要思想就讲完了,但是仍然有一些小细节需要注意:

  1. 此题要求输出最简分数,所以输出的时候分子分母要同时除以二者的最大公约数(gcd)。gcd 可以用欧几里得算法求得,属于基础内容,这里不做赘述。
  2. 考虑边界的问题:change 的时候,右边界必须减一,也就是要写成 change(1, l, r-1, c);,而查询的时候,是否减一都可以,因为即使不减一,最右边的收费站都不会被统计(对于最右边的收费站,式子 \(v_i \times (i-L+1)(R-i)\) 中 \((R-i)\) 这一项为 \(0\))。
  3. long long.

完整代码如下:

// P2221 [HAOI2012] 高速公路
#include<bits/stdc++.h>
#define lson (id << 1)
#define rson (id << 1 | 1)

using namespace std;

typedef long long ll;
const int MAXN = 1e5 + 10;
int n, m;
ll si[MAXN], sii[MAXN];// const. sum of 1~n, sum of 1^2~n^2
ll sum1, sum2, sum3; // only used in main
struct Node
{
	int left, right;
	ll lazy;
	ll sv, svi, svii; // sum of value, sum of value*i, sum of value*i*i
}t[MAXN << 2];

template<typename T> T gcd(T a, T b)
{
	if(b > a) return gcd(b, a);
	return b ? gcd(b, a % b) : a;
}

inline ll getlen(int id) {return t[id].right - t[id].left + 1;}
inline ll getsi(int id){return si[t[id].right] - si[t[id].left - 1];}
inline ll getsii(int id){return sii[t[id].right] - sii[t[id].left - 1];}

void update(int id)
{
	t[id].sv = t[lson].sv + t[rson].sv;
	t[id].svi = t[lson].svi + t[rson].svi;
	t[id].svii = t[lson].svii + t[rson].svii;
}

void pushdown(int id)
{
	if(t[id].lazy)
	{
		t[lson].sv += t[id].lazy * getlen(lson), t[rson].sv += t[id].lazy * getlen(rson);
		t[lson].svi += t[id].lazy * getsi(lson), t[rson].svi += t[id].lazy * getsi(rson);
		t[lson].svii += t[id].lazy * getsii(lson), t[rson].svii += t[id].lazy * getsii(rson);
		t[lson].lazy += t[id].lazy, t[rson].lazy += t[id].lazy;
		t[id].lazy = 0;
	}
}

void buildtree(int id, int l, int r)
{
	t[id].left = l, t[id].right = r;
	if(l == r) return; // sv,svi,svii = 0
	int mid = (l + r) >> 1;
	buildtree(lson, l, mid), buildtree(rson, mid + 1, r);
}

void change(int id, int l, int r, ll c)
{
	if(l == t[id].left && r == t[id].right)
	{
		t[id].lazy += c;
		t[id].sv += c*getlen(id), t[id].svi += c*getsi(id), t[id].svii += c*getsii(id);
		return;
	}
	pushdown(id);
	if(r <= t[lson].right) change(lson, l, r, c);
	else if(l >= t[rson].left) change(rson, l, r, c);
	else change(lson, l, t[lson].right, c), change(rson, t[rson].left, r, c);
	update(id);
}

void query(int id, int l, int r)
{
	if(l == t[id].left && r == t[id].right)
	{
		sum1 += t[id].sv;
		sum2 += t[id].svi;
		sum3 += t[id].svii;
		return;
	}
	pushdown(id);
	if(r <= t[lson].right) query(lson, l, r);
	else if(l >= t[rson].left) query(rson, l, r);
	else query(lson, l, t[lson].right), query(rson, t[rson].left, r);
}

int main()
{
	scanf("%d%d", &n, &m);
	buildtree(1, 1, n);
	for(ll i = 1; i <= n; i++) si[i] = si[i-1] + i, sii[i] = sii[i-1] + i*i;
	while(m--)
	{
		char str[5]; int l, r; ll c;
		scanf("%s%d%d", str, &l, &r);
		if(str[0] == 'C')
		{
			scanf("%lld", &c);
			change(1, l, r-1, c); // can not be change(1, l, r, c)
		}
		else if(str[0] == 'Q')
		{
			sum1 = sum2 = sum3 = 0;
			query(1, l, r-1); // query(1,l,r) is also ok 
			ll num = 1ll * (r-l+1) * (r-l) / 2, val = -sum3 + (l+r-1) * sum2 + (r-1ll*l*r) * sum1; // l*r > int 
			ll g = gcd(num, val), _num = num/g, _val = val/g;
			printf("%lld/%lld\n", _val, _num);
		}
	}
	return 0;
}

标签:lazy,int,题解,rson,收费站,高速公路,lson,HAOI2012,id
From: https://www.cnblogs.com/dengstar/p/17663655.html

相关文章

  • P2238 题解
    problem&blog。kkk的题解有一些地方是错的/cf,所以写篇题解造福后人。一眼DP,如果你平凡地设\(dp_{i,j}\),会发现买过的是不能再买的,然后就转移不动了。所以要记录每个点附近的点是否被吃过。于是状压,每个二进制位表示\((i,j)\)周围的一些点是否被吃过。不妨钦定\(X\)......
  • VSCode下载慢问题解决
    1.打开vscode官网浏览器搜索:vscodedownload或打开该网站https://code.visualstudio.com/Download/2.选中系统对应的版本 3.复制下载链接地址 4.修改链接地址将复制后的链接地址的域名(上图https后面框起来的那块)修改为 vscode.cdn.azure.cn最后变成类似:https......
  • 【题解 P4180】严格次小生成树
    [BJWC2010]严格次小生成树题目描述小C最近学了很多最小生成树的算法,Prim算法、Kruskal算法、消圈算法等等。正当小C洋洋得意之时,小P又来泼小C冷水了。小P说,让小C求出一个无向图的次小生成树,而且这个次小生成树还得是严格次小的,也就是说:如果最小生成树选择的边集......
  • [struts2]配置dispatcher INCLUDE和Forward可能问题解决
    Struts2.1.6GA不支持<dispatcher>FORWARD</dispatcher>和<dispatcher>INCLUDE</dispatcher>你要是和URLRewrite过滤器一起工作会报错。目前最新版本GeneralAvailability(GA)Releases-ReadyforPrimeTime!Struts2.1.8("bestavailable")Struts2.0.14(&qu......
  • JDK1.5在WIN7中显示时间不正确的问题解决
    最近发现一些新的windows操作系统中,JDK显示的时区不是正确的GMT+08的,而是默认的格林威治时间原以为是系统时区设置不对,但发现系统时间正确,时区也正确,就是JDK的不正确网上很多方法都是手动改tomcat设置,或者在代码中写死时区,这种做法都是治标不治本的于是继续查找根本所在后来几经比......
  • 关于win7图片查看很慢的问题解决
     找了很久都没找到解决方案,奇怪为什么网上没有人跟我遇到同样的问题?我对win7自带的图片和传真查看器一直是相当的不满意了,原来xp自带的版本,什么图片格式都能看可是win7里面,gif看不了(静态的),emf和wmf这种矢量图文件更是干脆都不支持最郁闷的是查看普通的jpg都很慢很慢。。。已经到......
  • P5629 【AFOI-19】区间与除法 题解
    P5629【AFOI-19】区间与除法题解由于题目中的运算是除法,所以对于一个数字\(x\),最多运算次数不会超过\(\lceil\log_{d}x\rceil\)就会变成\(0\)。然后我们就可以在\(O(n\logC)\)的时间复杂度内算出来每一个数字能被哪些原数消灭。这样处理询问仍然棘手,接下来有一个性质:......
  • SpringMVC3的ResponseBody返回字符串乱码问题解决
    SpringMVC的@ResponseBody注解可以将请求方法返回的对象直接转换成JSON对象,但是当返回值是String的时候,中文会乱码 原因是因为其中字符串转换和对象转换用的是两个转换器,而String的转换器中固定了转换编码为"ISO-8859-1" 网上也很多种解决方法,有通过配置Bean编码的,也有自己重写转......
  • 数位dp部分题解
    前言最近学了一种新的数位dp的状态表示,打算应用到以前做过的数位dp的题目。如果我们对数$N$进行数位dp,以前的状态定义$f(i,j)$表示所有数位大小为$i$且最高位是数字$j$的数的个数,如果还有其他约束条件那么再补充相应的状态即可。而新的状态定义则是$f(i,1)$和$f(i,0)$,其中$f(......
  • 销售基因链 题解
    销售基因链题目大意给定\(n\)个字符串,长度总和为\(m\),进行\(q\)次询问,每次询问给定两个字符串\(p,s\),问所有的字符串中以\(p\)为前缀且以\(s\)为后缀的有多少个。思路分析分享一个船新的答辩做法。(目前是最劣解)考虑哈希,对每个给定的字符串前后各做一遍哈希,将所有的......