【题解】做题记录(2022.12)
12.1
[ZJOI2007] 捉迷藏
题目分析:
简化一下其实就是动态维护树的直径。
考虑我们正常是怎么求的,就是通过一个 \(dp\),记录经过这个点的最长链和次长链,然后枚举每一个点计算答案就好了。
那么我们考虑修改操作会产生什么影响,也就是会对这个点的所有父亲产生影响,直接暴力改显然至少是 \(O(n^2)\) 的复杂度。
那么看到上面的这个:树上距离、枚举所有父亲,自然可以想到对原树建点分树,这样树深为 \(\log n\),而且上述的推理也都是成立的。
其实建完点分树之后就简单很多了,就是对于每一个点 \(x\) 它向它的父亲贡献一个值 \(v\),使得 \(v\) 是点分树上 \(x\) 的子树里距离它的父亲最远的一个点距离它父亲的距离。
这样直接对于每一个点求各个子树贡献的值的最大值和次大值求和就可以了。
这样我们就需要维护 \(B[x]\) 表示 \(x\) 在点分树上的所有儿子的子树对他的贡献的集合,同时维护所有的贡献,不仅仅维护最大值和次大值。
但是由于点分树的结构与原树很不同,也就是会有:
设 \(y = fa[x],z = fa[y]\),这里的 \(fa[i]\) 代表 \(i\) 在点分树上的父亲,\(dis(x,z) \not= dis(x,y) + dis(y,z)\)
所以需要再维护一个 \(A[x]\) 表示点分树上 \(x\) 这棵子树的所有节点到 \(x\) 的父亲的距离的集合。
由于需要动态修改,可以考虑用 \(set\) 但是 \(set\) 会被卡常,那么使用一下可删除的堆去维护就好了。
那么询问就是直接查询,修改就是暴力跳父亲更改。
代码:
点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 2e5+5;
const int INF = 1e9+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[2 * N];
struct my_que{
priority_queue<int> now,del;
int get_mx(){
while(!now.empty() && !del.empty() && now.top() == del.top()) now.pop(),del.pop();
if(now.empty()) return -INF;
return now.top();
}
void insert(int x){now.push(x);}
void delet(int x){
if(x == now.top()) now.pop();
else del.push(x);
}
int get_second(){
int a = get_mx();if(a == -INF) return -INF;
now.pop();int b = get_mx();now.push(a);
return b;
}
};
my_que A[N],B[N],ans;
//A 代表 x 到父亲的所有答案,B 代表儿子到当前点的最优答案
int n,cnt,len[N][22],deg[N],dist[N],head[N],son[N],sz[N],dep[N],fa[N],tfa[N],top[N],color[N];
bool vis[N];
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
int get_size(int now,int fath){
if(vis[now]) return 0;
int sz = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
sz += get_size(to,now);
}
return sz;
}
int get_root(int now,int fath,int tot,PII &rt){
if(vis[now]) return 0;
int sz = 1,mx = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
int tmp = get_root(to,now,tot,rt);
mx = max(mx,tmp);sz += tmp;
}
mx = max(mx,tot - sz);
if(mx < rt.second) rt = {now,mx};
return sz;
}
void calc(int now,int fath){
if(vis[now]) return;
PII rt = {now,n};
get_root(now,0,get_size(now,0),rt);now = rt.first;
vis[now] = true;tfa[now] = fath;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
calc(to,now);
}
}
void dfs1(int now,int fath){
dep[now] = dep[fath] + 1;fa[now] = fath;sz[now] = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dist[to] = dist[now] + e[i].val;
dfs1(to,now);
sz[now] += sz[to];
if(sz[to] > sz[son[now]]) son[now] = to;
}
}
void dfs2(int now,int topf){
top[now] = topf;
if(son[now]) dfs2(son[now],topf);
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fa[now] || to == son[now]) continue;
dfs2(to,to);
}
}
int lca(int x,int y){
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]) swap(x,y);
x = fa[top[x]];
}
if(dep[x] < dep[y]) return x;
return y;
}
int dis(int x,int y){
return dist[x] + dist[y] - 2 * dist[lca(x,y)];
}
void change(int now,int opt){
if(B[now].get_second() == -INF) return;
int tmp = B[now].get_mx() + B[now].get_second();
if(opt) ans.insert(tmp);
else ans.delet(tmp);
}
void insert(int x){
int now = x,pre = 0,tot = 0;
change(x,0);B[x].insert(0);change(x,1);
while(now){
change(now,0);
if(pre){
if(A[pre].get_mx() != -INF) B[now].delet(A[pre].get_mx());
A[pre].insert(len[x][tot]);
if(A[pre].get_mx() != -INF) B[now].insert(A[pre].get_mx());
}
change(now,1);
pre = now,now = tfa[now];++tot;
}
}
void delet(int x){
int now = x,pre = 0,tot = 0;
change(x,0);B[x].delet(0);change(x,1);
while(now){
change(now,0);
if(pre){
if(A[pre].get_mx() != -INF) B[now].delet(A[pre].get_mx());
A[pre].delet(len[x][tot]);
if(A[pre].get_mx() != -INF) B[now].insert(A[pre].get_mx());
}
change(now,1);
pre = now,now = tfa[now];++tot;
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
n=read();
for(int i=1; i<n; i++){
int from,to;from=read();to=read();
add_edge(from,to,1);add_edge(to,from,1);
}
int tmp = 0;
calc(1,0);dfs1(1,0);dfs2(1,1);
for(int i=1; i<=n; i++){
int tot = 0,now = i;
while(now){
len[i][tot] = dis(now,i);
now = tfa[now],++tot;
}
}
for(int i=1; i<=n; i++) insert(i);
tmp = n;
int q;q=read();
while(q--){
char opt[2];
scanf("%s",opt + 1);
if(opt[1] == 'C'){
int x;x=read();
if(color[x] == 0) tmp--,delet(x);
else tmp++,insert(x);
color[x] ^= 1;
}
else{
if(tmp == 1) printf("0\n");
else if(tmp == 0) printf("-1\n");
else printf("%d\n",max(ans.get_mx(),0));
}
}
return 0;
}
[ZJOI2015]幻想乡战略游戏
题目分析:
显然我们选择重心就是答案。
我们可以发现如果我们当前点为 \(x\),而重心为 \(y\),那么我们沿着树上 \((x,y)\) 的路径走我们的答案一定是逐渐变优的,而不这么走一定是逐渐变劣的,可以使用重心的性质进行证明。
考虑怎么快速维护答案,答案就是求一个子树内所有的点到他的距离和,就是一个点分树的板子题,就用点分树就好了。
那么就可以考虑暴力一点,每一个点去找它的儿子里哪个是变优的,然后走过去,但是这样走的话复杂度显然是不对的,但是我们可以使用类似二分的思想,每次不是走到那个儿子,而是走到对应的子树的重心,也就是点分树上对应的子树的根节点,这样我们最多走 \(\log n\) 步就可以走到,复杂度就降下去了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 2e5+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[2 * N];
int n,q,cnt,a_rt,head[N],sz[N],top[N],A[N],B[N],tfa[N],dep[N],fa[N],dist[N],son[N];
bool vis[N];
vector<PII> v[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
int get_size(int now,int fath){
if(vis[now]) return 0;
int sz = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
sz += get_size(to,now);
}
return sz;
}
int get_root(int now,int fath,int tot,PII &rt){
if(vis[now]) return 0;
int sz = 1,mx = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
int tmp = get_root(to,now,tot,rt);
sz += tmp;mx = max(mx,tmp);
}
mx = max(mx,tot - sz);
if(mx < rt.second) rt = {now,mx};
return sz;
}
void calc(int now,int fath){
if(vis[now]) return;
PII rt = {now,n};int pre = now;
get_root(now,0,get_size(now,0),rt);now = rt.first;
vis[now] = true;tfa[now] = fath;v[fath].push_back({pre,now});
// printf("%lld %lld\n",tfa[now],now);
if(!a_rt) a_rt = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
calc(to,now);
}
}
void dfs1(int now,int fath){
sz[now] = 1;dep[now] = dep[fath] + 1;fa[now] = fath;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dist[to] = dist[now] + e[i].val;
dfs1(to,now);
sz[now] += sz[to];
if(sz[to] > sz[son[now]]) son[now] = to;
}
}
void dfs2(int now,int topf){
top[now] = topf;
if(son[now]) dfs2(son[now],topf);
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fa[now] || to == son[now]) continue;
dfs2(to,to);
}
}
int lca(int x,int y){
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]) swap(x,y);
x = fa[top[x]];
}
if(dep[x] < dep[y]) return x;
return y;
}
int dis(int x,int y){
return dist[x] + dist[y] - 2 * dist[lca(x,y)];
}
void modify(int x,int val){
int pre = 0,now = x;
while(now){
sz[now] += val;A[now] += val * dis(x,now);
if(pre) B[pre] += val * dis(x,now);
pre = now,now = tfa[now];
}
}
int get_ans(int x){
int now = x,pre = 0,ans = 0;
while(now){
ans += A[now] - B[pre] + dis(now,x) * (sz[now] - sz[pre]);
pre = now,now = tfa[now];
}
return ans;
}
int query(int now){
int ans = get_ans(now);
// printf("calc(%d) = %d\n",now,ans);
for(auto j : v[now]){
int tmp = get_ans(j.first);
if(tmp < ans) return query(j.second);
}
return ans;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld",&n,&q);
for(int i=1; i<n; i++){
int from,to,val;
scanf("%lld%lld%lld",&from,&to,&val);
add_edge(from,to,val);add_edge(to,from,val);
}
calc(1,0);dfs1(1,0);dfs2(1,1);
for(int i=1; i<=n; i++) sz[i] = 0;
// for(int i=1; i<=n; i++){
// printf("fa[%d] = %d ",i,tfa[i]);
// printf("v[%d] = ",i);
// for(auto j : v[i]) printf("%d ",j.second);
// printf("\n");
// }
while(q--){
int u,e;
scanf("%lld%lld",&u,&e);
modify(u,e);
printf("%lld\n",query(a_rt));
}
return 0;
}
12.2
P5311 [Ynoi2011] 成都七中
题目分析:
这个操作看到应该都会一脸懵逼吧,看上去就很不能维护。
我们考虑怎么样能让这个操作看上去可以维护:若 \(y\) 与 \(x\) 位于同一连通块内,则对于树上 \((x,y)\) 路径上的点的编号一定全部在 \([l,r]\) 里,就是设 \(mn\) 为路径上节点编号的最小值,\(mx\) 为路径上节点编号的最大值,一定有 \(l \le mx \and mn \le r\)。
这样看上去就好一些了,但是还是不大行。我们会发现一种很神奇的暴力,我们找到 \(x\) 最浅的父亲 \(z\),使得 \(z\) 与 \(x\) 联通,那么连通块内有的点就是 \(z\) 的子树与 \(z\) 联通的点。
树上路径、暴力跳父亲,可以想到点分树,因为对于原树的任意一个连通块在点分树内必然存在一个节点,使得原树上连通块内的所有点都在点分树上这个点的子树内,所以对于我们上述在原树中的所有得到的结论在点分树上依然成立。
那么我们对于每一个点,维护点分树内其子树内的所有点到他的路径的节点编号的最大值、最小值、该节点颜色就好了,因为这个信息可以在建点分树的时候一遍 \(dfs\) 求出,所以就不用建点分树了,当然为了方便我们需要记录一下每个点在点分树上的所有父亲的信息。
我们发现按如果在线其实不好做,那么就离线一下,就是将每个询问挂到点分树上与它联通并且深度最浅的祖先上,这样对于每一个点做一下求一下不同颜色数就好了。
那么具体怎么求这个值呢,我们发现一个点的信息 \((L,R)\) 面对一个询问 \((l,r)\) 是符合条件的就是 \(l \le L \and R \le r\),那么我们就按 \(l\) 从大到小排序,然后每次询问 \(\le r\) 的信息就好了, 可以对于每一种颜色维护 \(mn[i]\) 表示 \(i\) 这种颜色的已经出现的节点的最小的 \(R\) 是多少,然后对于 \(mn[i]\) 的位置算上 \(i\) 这种颜色的贡献就好了,可以发现这样做答案就是最终的答案。
代码:
点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 2e5+5;
const int INF = 1e9+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[2 * N];
struct node{
int l,r,id,opt;
};
vector<node> v[N],p[N];
int n,cnt,color[N],sum[N],tfa[N],head[N],mn[N],ans[N];
bool vis[N];
bool cmp(node a,node b){
if(a.l != b.l) return a.l > b.l;
return a.opt < b.opt;
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
int get_size(int now,int fath){
if(vis[now]) return 0;
int sz = 1;
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
sz += get_size(to,now);
}
return sz;
}
int get_root(int now,int fath,int tot,PII &rt){
if(vis[now]) return 0;
int sz = 1,mx = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
int tmp = get_root(to,now,tot,rt);
sz += tmp;mx = max(mx,tmp);
}
mx = max(mx,tot - sz);
if(mx < rt.second) rt = {now,mx};
return sz;
}
void solve(int now,int fath,int l,int r,int root){
if(vis[now]) return;
v[root].push_back({l,r,color[now],0});
p[now].push_back({l,r,root,0});
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
solve(to,now,min(l,to),max(r,to),root);
}
}
void calc(int now,int fath){
if(vis[now]) return;
PII rt = {now,n};
get_root(now,0,get_size(now,0),rt);now = rt.first;
solve(now,0,now,now,now);
vis[now] = true;tfa[now] = fath;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
calc(to,now);
}
}
int query(int x){
int ans = 0;
for(;x;x -= x & (-x)) ans += sum[x];
return ans;
}
void modify(int x,int val){
for(;x <= n;x += x & (-x)) sum[x] += val;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
memset(mn,0x3f,sizeof(mn));
int q;scanf("%d%d",&n,&q);
for(int i=1; i<=n; i++) scanf("%d",&color[i]);
for(int i=1; i<n; i++){
int from,to;
scanf("%d%d",&from,&to);
add_edge(from,to);add_edge(to,from);
}
calc(1,0);
for(int i=1; i<=q; i++){
int l,r,x;
scanf("%d%d%d",&l,&r,&x);
int pos = x;
for(int j=0; j<(int)p[x].size(); j++){
int mn = p[x][j].l,mx = p[x][j].r;
if(l <= mn && mx <= r){
pos = p[x][j].id;
break;
}
}
v[pos].push_back({l,r,i,1});
// printf("%d\n",pos);
}
for(int i=1; i<=n; i++){
sort(v[i].begin(),v[i].end(),cmp);
// printf("%d:",i);
for(int j = 0; j<(int)v[i].size(); j++){
// printf("(%d,%d,%d) ",v[i][j].l,v[i][j].r,v[i][j].id);
if(v[i][j].opt == 1) ans[v[i][j].id] = query(v[i][j].r);
else if(v[i][j].r < mn[v[i][j].id]){
modify(mn[v[i][j].id],-1);mn[v[i][j].id] = v[i][j].r;
modify(mn[v[i][j].id],1);
}
}
for(int j=0; j<(int)v[i].size(); j++){
if(v[i][j].opt == 0 && v[i][j].r == mn[v[i][j].id]){
modify(mn[v[i][j].id],-1);mn[v[i][j].id] = INF;
}
}
// printf("\n");
}
for(int i=1; i<=q; i++) printf("%d\n",ans[i]);
return 0;
}
12.3
Luogu P5888 传球游戏
题目分析:
有一个非常显然的状态:\(dp[i][j]\) 表示前 \(i\) 次传球,最后球在 \(j\) 这里的方案数。转移就是从前面所有的点加和过来,然后减去限制掉的点。
我们发现状态数太多了,但是也可以发现这是没有必要的,因为有特殊转移的点只是有限制的点和 \(1\),对于其他的点每次都是前面所有点的和。
所以我们只需要维护这些特殊点的值,对于其他的点的值我们使用一个变量来维护。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int MOD = 998244353;
int tot,b[N],x[N],y[N],f[N],g[N];
vector<int> v[N];
int mod(int x){
return (x % MOD + MOD)%MOD;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m,k;
scanf("%lld%lld%lld",&n,&m,&k);
b[++tot] = 1;
for(int i=1; i<=k; i++){
scanf("%lld%lld",&x[i],&y[i]);
b[++tot] = x[i];b[++tot] = y[i];
}
sort(b+1,b+tot+1);tot = unique(b+1,b+tot+1) - b - 1;
for(int i=1; i<=k; i++){
x[i] = lower_bound(b+1,b+tot+1,x[i]) - b;
y[i] = lower_bound(b+1,b+tot+1,y[i]) - b;
if(x[i] == y[i]) continue;
v[y[i]].push_back(x[i]);
}
for(int i=1; i<=tot; i++) v[i].push_back(i);
int cnt = n - tot,tmp = 0;
f[1] = 1;
for(int i=1; i<=m; i++){
int sum = mod(cnt * tmp);
for(int j=1; j<=tot; j++) sum = mod(sum + f[j]);
tmp = mod(sum - tmp); //tmp 即非特殊点的 dp 值
for(int j=1; j<=tot; j++){
g[j] = sum;
for(int k : v[j]) g[j] = mod(g[j] - f[k]);
}
for(int j=1; j<=tot; j++){
f[j] = g[j],g[j] = 0;
// printf("%lld ",f[j]);
}
// printf("\n");
}
printf("%lld\n",f[1]);
return 0;
}
Luogu P5889 跳树
题目分析:
显然需要使用线段树维护,那么就考虑维护什么了。
我们维护三个值:当前一段最高可以跳到第几辈祖先\((up)\)、最终可以跳到这个祖先的哪个儿子\((down)\),到达这个儿子的路径是什么样子的\((len)\)。
这里所谓的路径是什么样子的,就是说我们可以将一次跳左儿子视为 \(0\),一次跳右儿子视为 \(1\),这样就可以通过一个 \(01\) 串来代表是怎么跳到这个儿子的。
那么假设我们从 \(x\) 点开始跳,我们最终到达的位置就是 \(\max(1,x > up) < down + len\),这里的 \(<\) 和 \(>\) 指的是二进制下的左移右移。
对于区间合并就是简单的分类讨论一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
struct node{
int up,down,len;
//记录最高到第几辈父亲,最后到了最高父亲的第几辈儿子,到上述儿子的路径
node(){up = 0,down = 0,len = 0;}
node(int a,int b,int c){up = a,down = b,len = c;}
}tree[4 * N];
int opt[N];
node merge(node a,node b){
node ans;
if(!a.up && !a.down) ans = b;
else if(!b.up && !b.down) ans = a;
else if(a.down > b.up) ans.up = a.up,ans.down = a.down - b.up + b.down,ans.len = ((a.len>>b.up)<<b.down)+b.len;
else ans.up = a.up + b.up - a.down,ans.down = b.down,ans.len = b.len;
return ans;
}
void change(int now,int opt){
tree[now] = node(0,0,0);
if(opt == 1) tree[now].down = 1;
else if(opt == 2) tree[now].down = 1,tree[now].len = 1;
else if(opt == 3) tree[now].up = 1;
}
void build(int now,int now_l,int now_r){
if(now_l == now_r){
change(now,opt[now_l]);
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);build(now<<1|1,mid+1,now_r);
tree[now] = merge(tree[now<<1],tree[now<<1|1]);
}
node query(int now,int now_l,int now_r,int l,int r){
if(l <= now_l && r >= now_r) return tree[now];
node ans = node(0,0,0);
int mid = (now_l + now_r)>>1;
if(l <= mid) ans = merge(ans,query(now<<1,now_l,mid,l,r));
if(r > mid) ans = merge(ans,query(now<<1|1,mid+1,now_r,l,r));
return ans;
}
void modify(int now,int now_l,int now_r,int pos,int opt){
if(now_l == now_r){
change(now,opt);
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) modify(now<<1,now_l,mid,pos,opt);
else modify(now<<1|1,mid+1,now_r,pos,opt);
tree[now] = merge(tree[now<<1],tree[now<<1|1]);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m,q;
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1; i<=m; i++) scanf("%lld",&opt[i]);
build(1,1,m);
while(q--){
int opt,x,y,z;
scanf("%lld",&opt);
if(opt == 1){
scanf("%lld%lld%lld",&x,&y,&z);
node ans = query(1,1,m,y,z);
int res = (max(1ll,x>>ans.up)<<ans.down)+ans.len;
printf("%lld\n",res);
}
else if(opt == 2){
scanf("%lld%lld",&x,&y);
modify(1,1,m,x,y);
}
}
return 0;
}
Luogu P6015 [CSGRound3]游戏
题目分析:
一个显然的想法是枚举每一个 \(X\),然后去判断是否成立,但是显然复杂度不对。
我们可以换一个想法,枚举小 \(Z\) 取那些牌,然后判断在取这些牌的基础上有哪些 \(X\) 是必胜的。
假设我们取了前 \(i\) 张牌,和为 \(s_1\),那么我们就是要找到第一个大于 \(i\) 的位置 \(j\) 使得 \((i,j]\) 区间内的牌的数值和大于等于 \(s_1\),不妨记这个数为 \(s_2\),可以发现此时 \(X \in [s_1,s_2)\) 就是必胜的,因为这样可以保证 \(s_1\) 一定不变,而能取到的大于 \(s_1\) 的值都会变成 \(0\)。
当然对于 \(s_2 = s_1\) 的情况验证也会发现是合法的。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e6+5;
int a[N],pre[N],val[N];
int main(){
int n;scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
for(int i=1; i<=n; i++) pre[i] = pre[i-1] + a[i];
int k;scanf("%d",&k);
int pos = 0;
for(int i=0; i<=n; i++){
if(pre[i] > k) break;
while(pos <= n && pre[pos] - pre[i] < pre[i]) ++pos;
val[pre[i]]++;
if(pre[pos] - pre[i] >= pre[i]) val[pre[pos] - pre[i]]--;
}
for(int i=1; i<=k; i++) val[i] += val[i-1];
vector<int> ans;
for(int i=1; i<=k; i++) if(val[i] > 0) ans.push_back(i);
printf("%d\n",(int)ans.size());
for(int i : ans) printf("%d ",i);
return 0;
}
Luogu P6102 [EER2]谔运算
题目分析:
以为是一个神仙题,但是其实不难。
这种题显然需要考虑拆位计算,对于第 \(i\) 位有贡献的方案数显然就是:
- 或起来为 \(1\) 的方案数 乘以 与起来为 \(0\) 的方案数
- 或起来为 \(0\) 的方案数 乘以 与起来为 \(1\) 的方案数
预处理一下多少个数这一位为 \(0\) 多少个数这一位为 \(1\) 就可以很简单地算出来了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+6;
int a[N],tmp[N];
signed main(){
int n;scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
unsigned int ans = 0;
for(int i=0; i<=31; i++){
for(int j=1; j<=n; j++) tmp[j] = (a[j] >> i)&1;
unsigned int cnt[3] = {0,0,0};
for(int j=1; j<=n; j++) cnt[tmp[j]]++;
unsigned int res = (cnt[0] * cnt[0]) * (cnt[1] * cnt[1]);
res += ((cnt[0] + cnt[1]) * cnt[1]) * (cnt[0] * (cnt[0] + cnt[1]));
ans += res << i;
}
cout<<ans * 2<<endl; //因为是有顺序的
return 0;
}
12.4
Luogu P6016 [CSGRound3]出游
题目分析:
看到 \(T\) 的范围想到应该需要矩阵快速幂。
我们可以考虑记 \(M_{i,j}\) 代表若 \(i\) 去,那么第一天 \(j\) 是否会去。
这样就出现了一个奇妙的事情,\(M^{k}_{i,j}\) 就代表若 \(i\) 去,那么第 \(k\) 天 \(j\) 是否会去。
那么我们矩阵快速幂得到 \(M^T\) 后,算出每一个人去的概率,最后加和就是答案了。
需要特判 \(T = 0\) 的情况。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505;
const int MOD = 998244353;
int n,T,p[N],f[N];
struct matrix{
bitset<N> M[N]; //M[i][j] 代表如果 i 第 0 天去了,那么第 k 天 j 是否会去
void operator *= (matrix b){
bitset<N> ans[N],tmp[N];
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
tmp[i][j] = b.M[j][i];
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
ans[i][j] = (M[i] & tmp[j]).any();
}
}
for(int i=1; i<=n; i++){
M[i] = ans[i];
}
}
}tmp;
matrix power(int b){
b --;
matrix res = tmp,a = tmp;
while(b){
if(b & 1) res *= a;
a *= a;
b >>= 1;
}
return res;
}
int mod(int x){
return (x % MOD + MOD)%MOD;
}
signed main(){
scanf("%lld%lld",&n,&T);
for(int i=1; i<=n; i++){
scanf("%lld",&p[i]);
int sz;scanf("%lld",&sz);
for(int j=1; j<=sz; j++){
int x;scanf("%lld",&x);
tmp.M[x][i] = 1;
}
}
if(T == 0){
int ans = 0;
for(int i=1; i<=n; i++) ans = mod(ans + p[i]);
printf("%lld\n",ans);
}
else{
matrix ans = power(T);
for(int i=1; i<=n; i++) f[i] = 1;
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
if(ans.M[i][j] == 1){
f[j] = mod(f[j] * (1 - p[i]));
}
}
}
for(int i=1; i<=n; i++) f[i] = mod(1 - f[i]);
int res = 0;
for(int i=1; i<=n; i++) res = mod(res + f[i]);
printf("%lld\n",res);
}
return 0;
}
Luogu P6103 [EER2]直接自然溢出啥事没有
题目分析:
可以发现,如果设 \(a[i]\) 表示长度为 \(i\) 的字符串合法的程序片段的数量,那么必然需要知道 \(c[i]\) 表示长度为 \(i\) 的合法的语句块的数量,然后层层下去,就可以设出来这么一些东西:
\(a[i]\) 代表程序片段的数量,\(b[i]\) 代表语句的数量,\(c[i]\) 代表语句块的数量,\(d[i]\) 代表函数的数量,\(e[i]\) 代表值的数量。
然后具体的递推关系就是按照题目里说的来就好了。
需要注意的有两点:
- 长度为 \(1\) 的程序片段的数量为 \(1\)
- \(e[i]\) 不需要加 \(d[i-2]\),因为他们都有 \(()\) 的转移所以会重复,显然 \(e[i-2]\) 一定包含所有的 \(d[i-2]\) 的情况,所以把 \(d[i-2]\) 去掉。
代码:
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int N = 2e5+5;
ull a[N],b[N],c[N],d[N],e[N];
//分别为:程序片段、语句、语句块、 函数、值
int main(){
int n;scanf("%d",&n);
a[0] = 1;a[1] = 1;b[1] = 1;
for(int i=2; i<=n; i++){
c[i] = a[i-2];
d[i] = c[i-2] + (i >= 4 ? c[i-4] : 0) + d[i-2];
e[i] = e[i-2] + d[i] + d[i-2] - d[i-2];
b[i] = c[i] + e[i-1];
for(int j=1; j<=i; j++) a[i] += a[i-j] * b[j];
}
printf("%llu\n",a[n]);
return 0;
}
Luogu P6196 [EER1]代价
题目分析:
我们可以发现我们最优的策略肯定是从 \(1\) 开始从外向里删除,那么我们就可以以 \(1\) 为界,每两个相邻的 \(1\) 中间的部分单独处理。
首先 \(\sum a[i] \times a[i+1]\) 的贡献肯定少不了,我们发现删到最后就是只会剩下一个数,这样我们唯一可以改变的就是这一个数的值。
我们显然可以控制删除的顺序使得最后剩下的就是这一块的最小值,所以最终一块内的答案就是:\(mn + \sum a[i] \times a[i+1]\)
最后需要加上把 \(1\) 全部删去的代价,也就是 \(1\) 的个数。
代码:
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int N = 2e6+6;
const ull INF = 1e9+5;
ull a[N];
ull get_ans(int l,int r){
ull ans = 0,mn = a[r];
for(int i=l; i<r; i++) mn = min(mn,a[i]),ans += a[i] * a[i+1];
ans += l <= r ? mn : 0;
return ans;
}
int main(){
int n;scanf("%d",&n);
a[0] = a[n+1] = 1;
for(int i=1; i<=n; i++) scanf("%llu",&a[i]);
int pos = 0;
ull ans = 0;
for(int i=0; i<=n; i = pos){
++pos;
for(;pos <= n+1;pos ++) if(a[pos] == 1) break;
ans += get_ans(i+1,pos-1);
}
for(int i=1; i<=n; i++){
if(a[i] == 1) ans++;
}
printf("%llu\n",ans);
return 0;
}
12.5
Luogu P6381 『MdOI R2』Odyssey
题目分析:
我们可以想到使用拓扑排序和 \(dp\) 解决,\(dp\) 的话就是设 \(dp[i][j]\) 表示以 \(i\) 结束的路径最后一条边权值为 \(j\) 的最长路径长度。
但是这样转移就会很麻烦,但是我们发现可以对于每一个权值预处理一下,因为如果 \(ab = i^k\) 也就是说他们质因数分解之后对应数指数相加为 \(k\) 的倍数,那么我们就可以提前质因数分解一下然后将指数模 \(k\),这样每一个数对应的可以形成完美数对的数就是一个定值,这样 \(dp\) 转移就非常简单了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
struct edge{
int nxt,to,val,len;
edge(){}
edge(int _nxt,int _to,int _val,int _len){
nxt = _nxt,to = _to,val = _val,len = _len;
}
}e[2 * N];
int n,m,k,cnt,head[N],deg[N];
map<int,int> dp[N],match; //设 dp[i][j] 表示以 i 这个点结尾,最后一条边为 j 的最长路径长度
void add_edge(int from,int to,int val,int len){
e[++cnt] = edge(head[from],to,val,len);
head[from] = cnt;
}
int get(int x){
int ans1 = 1,ans2 = 1;
for(int i=2; i*i<=x; i++){
if(x % i == 0){
int cnt = 0;
while(x % i == 0) ++cnt,x /= i;
cnt %= k;
if(cnt != 0){
for(int j=1; j<=cnt; j++) ans1 *= i;
for(int j=1; j<=k-cnt; j++) ans2 *= i;
}
}
}
if(x > 1){
int cnt = 1;cnt %= k;
if(cnt != 0){
for(int j=1; j<=cnt; j++) ans1 *= x;
for(int j=1; j<=k-cnt; j++) ans2 *= x;
}
}
match[ans1] = ans2;
return ans1;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1; i<=m; i++){
int from,to,val,len;
scanf("%lld%lld%lld%lld",&from,&to,&val,&len);
deg[to]++;val = get(val);
add_edge(from,to,val,len);
}
int mx = 0;
queue<int> q;
for(int i=1; i<=n; i++){
if(!deg[i]) q.push(i);
}
while(!q.empty()){
int now = q.front();q.pop();
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to,val = e[i].val;
dp[to][val] = max(dp[to][val],e[i].len);
if(dp[now].count(match[val]))
dp[to][val] = max(dp[to][val],dp[now][match[val]] + e[i].len);
mx = max(mx,dp[to][val]);
deg[to]--;
if(!deg[to]) q.push(to);
}
}
printf("%lld\n",mx);
return 0;
}
Luogu P6583 回首过去
题目分析;
只要考虑清楚了什么是有限小数,那么一切都简单了。
稍微思考一下就会发现,有限小数一定可以写成类似这样的形式:
满足 \(2 \nmid c\) 且 \(5 \nmid c\)
可以考虑枚举 \(a,b\),那么 \(c\) 的取值范围就是 \([1,\lfloor \frac{n}{2^a\times 5^b} \rfloor]\),那么对于每一个 \(c\) 符合条件的 \(d\) 就有 \(\lfloor \frac{n}{c} \rfloor\) 种,那么我们其实就是求解这样一个式子:
直接整除分块就好了,但是这样的复杂度就是 \(O(\log^2n \sqrt{n})\),实测不能过。
所以就预处理一下对于每一个上界答案是多少就好了,显然一遍整除分块就可以解决,复杂度 \(O(\log^2 n + \sqrt{n})\)
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
unordered_map<int,int> mp;
int calc(int limit){return mp[limit];}
signed main(){
scanf("%lld",&n);
int tmp = 0;
for(int l=1,r=0; l<=n; l = r + 1){
r = n / (n / l);
int res = (r/2) - ((l-1)/2) + (r/5) - ((l-1)/5) - ((r / 10) - ((l-1)/10));
tmp += (r - l + 1 - res) * (n / l);
mp[r] = tmp;
}
int ans = 0;
for(int tmp1=1; tmp1 <= n; tmp1 *= 2){
for(int tmp2=1; tmp2 * tmp1 <= n; tmp2 *= 5){
ans += calc(n / (tmp1 * tmp2));
}
}
printf("%lld\n",ans);
return 0;
}
12.6
Luogu P6584 重拳出击
题目分析:
我们胜利的条件是什么:距离他最远的一个 \(youyou\) 离他的距离小于等于 \(k\)。
可以发现假设我们在 \(x\) 上,下一步走到 \(y\),那么会对这些距离产生什么影响,对于以 \(x\) 为根下 \(y\) 的子树内的所有 \(youyou\) 的距离都会减 \(2\),对于其他位置的 \(youyou\) 距离不变。
那么就可以得到一个贪心策略,每次去朝着距离的最大值最大的一个子树走,显然可以树剖快速维护。
但是这样的策略不是很对,因为假设我们有多个子树的距离的最大值均为我们的最大的最大值那么怎么办呢?
我们此时发现呆着不动就是最优的选择,因为我们呆在原地两个回合就可以使得所有的子树内的距离都减二,显然比我们一个个去跑要优。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[2 * N];
int cnt,tot,head[N],tag[4 * N],mx[4 * N],sz[N],fa[N],dep[N],dfn[N],val[N];
bool flag[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
sz[now] = 1,fa[now] = fath,dep[now] = dep[fath] + 1,dfn[now] = ++tot;
val[tot] = flag[now] ? dep[now] : 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
sz[now] += sz[to];
}
}
void pushup(int now){
mx[now] = max(mx[now<<1],mx[now<<1|1]);
}
void pushdown(int now){
if(tag[now] == 0) return;
mx[now<<1] = max(mx[now<<1] + tag[now],0);mx[now<<1|1] = max(mx[now<<1|1] + tag[now],0);
tag[now<<1] += tag[now];tag[now<<1|1] += tag[now];
tag[now] = 0;
}
void build(int now,int now_l,int now_r){
if(now_l == now_r){
mx[now] = val[now_l];
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);build(now<<1|1,mid+1,now_r);
pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
if(l > r) return 0;
if(l <= now_l && r >= now_r) return mx[now];
pushdown(now);
int mid = (now_l + now_r)>>1,ans = 0;
if(l <= mid) ans = max(ans,query(now<<1,now_l,mid,l,r));
if(r > mid) ans = max(ans,query(now<<1|1,mid+1,now_r,l,r));
pushup(now);
return ans;
}
void modify(int now,int now_l,int now_r,int l,int r,int val){
if(l > r) return;
if(l <= now_l && r >= now_r){
mx[now] = max(0,mx[now] + val);
tag[now] += val;
return;
}
pushdown(now);
int mid = (now_l + now_r)>>1;
if(l <= mid) modify(now<<1,now_l,mid,l,r,val);
if(r > mid) modify(now<<1|1,mid+1,now_r,l,r,val);
pushup(now);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;scanf("%d",&n);
for(int i=1; i<n; i++){
int from,to;scanf("%d%d",&from,&to);
add_edge(from,to);add_edge(to,from);
}
int m;scanf("%d",&m);
for(int i=1; i<=m; i++){
int p;scanf("%d",&p);
flag[p] = true;
}
int k,x;scanf("%d%d",&k,&x);
dep[0] = -1;
dfs(x,0);
build(1,1,n);
int now = x,T = 0;
while(1){
// printf("%d\n",now);
++T;
int mx = 0,pos = 0;
bool p = false;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fa[now]) continue;
int tmp = query(1,1,n,dfn[to],dfn[to] + sz[to] - 1);
if(tmp > mx) mx = tmp,pos = to,p = false;
else if(tmp == mx) p = true;
}
int tmp = max(query(1,1,n,1,dfn[now]-1),query(1,1,n,dfn[now]+sz[now],n));
if(tmp > mx) mx = tmp,pos = fa[now],p = false;
else if(tmp == mx) p = true;
if(mx <= k) break;
if(p){
modify(1,1,n,1,n,-1);
continue;
}
if(pos == fa[now]) modify(1,1,n,1,dfn[now]-1,-2),modify(1,1,n,dfn[now]+sz[now],n,-2);
else modify(1,1,n,dfn[pos],dfn[pos] + sz[pos] - 1,-2);
now = pos;
}
printf("%d\n",T);
return 0;
}
Luogu P6687 论如何玩转 Excel 表格
题目分析:
我们可以发现无论如何操作,原来在同一列的数还是在同一列,只不过一次操作就会交换一次位置罢了。
那么方案数显然就是逆序对数啊,就是将第一个表格按第二个表格排序需要的最小操作次数,就是逆序对数量。
那么判无解也就很好判了:
- 原本在同一列的,在另一个表格不在同一列
- 对于两个匹配的位置,他们距离为奇数,但是顺序相同
- 对于两个匹配的位置,他们的距离为偶数,但是顺序相反
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
int n,a[N],b[N],c[N],d[N],match[N],sum[N],mp[N];
void modify(int x,int val){
for(;x<=n; x += x & (-x)) sum[x] += val;
}
int query(int x){
int ans = 0;
for(;x;x -= x & (-x)) ans += sum[x];
return ans;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=1; i<=n; i++) scanf("%lld",&b[i]);
for(int i=1; i<=n; i++) scanf("%lld",&c[i]),mp[c[i]] = i;
for(int i=1; i<=n; i++) scanf("%lld",&d[i]),mp[d[i]] = i + n;
bool flag = true;
for(int i=1; i<=n; i++){
if(mp[a[i]] > n){
if(mp[b[i]] + n == mp[a[i]] && abs((i - mp[b[i]]) % 2) == 1) match[i] = mp[b[i]];
else flag = false;
}
else if(mp[b[i]] > n){
if(mp[a[i]] + n == mp[b[i]] && (i - mp[a[i]]) % 2 == 0) match[i] = mp[a[i]];
else flag = false;
}
else flag = false;
}
if(!flag){
printf("dldsgay!!1\n");
return 0;
}
int ans = 0;
for(int i=1; i<=n; i++){
modify(match[i],1);
ans += i - query(match[i]);
}
printf("%lld\n",ans);
return 0;
}
Luogu P6688 可重集
题目分析:
考虑使用哈希,也就是每一个数赋一个权值,判断区间权值是否相等,相等则代表区间相同。
为了方便进行加法操作,也为了不重复,我们可以给数 \(x\) 赋 \(P^x\) 作为它的权值,其中 \(P\) 是我们任意指定的数,但最好为质数。
这样的权值使得我们对于加 \(k\) 操作其实就是乘以 \(P^k\) 就好了。
对于 \(k\) 我们发现其实很唯一的,可以直接使用区间和来求解。
那么就用两棵树状数组,一个维护区间和,一个维护区间哈希值的和到时候算一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
const int p = 213;
const int N = 2e6+5;
int a[N],n,q;
int mod(int x){
return ((x % MOD) + MOD)%MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = mod(res * a);
a = mod(a * a);
b >>= 1;
}
return res;
}
struct BIT{
int sum[N],opt;
void modify(int x,int val){
if(opt == 1) for(;x <= n; x += x & (-x)) sum[x] = sum[x] + val;
if(opt == 2) for(;x <= n; x += x & (-x)) sum[x] = mod(sum[x] + val);
}
int query(int x){
int ans = 0;
if(opt == 1) for(;x; x -= x & (-x)) ans = ans + sum[x];
if(opt == 2) for(;x; x -= x & (-x)) ans = mod(ans + sum[x]);
return ans;
}
int get(int l,int r){
if(opt == 1) return query(r) - query(l-1);
if(opt == 2) return mod(query(r) - query(l-1));
}
}T1,T2;
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld",&n,&q);
T1.opt = 1,T2.opt = 2;
for(int i=1; i<=n; i++){
scanf("%lld",&a[i]);
T2.modify(i,power(p,a[i]));T1.modify(i,a[i]);
}
while(q--){
int opt;
scanf("%lld",&opt);
if(opt == 0){
int x,y;scanf("%lld%lld",&x,&y);
T1.modify(x,-a[x]);T1.modify(x,y);
T2.modify(x,-power(p,a[x]));T2.modify(x,power(p,y));
a[x] = y;
}
else{
int l1,r1,l2,r2;
scanf("%lld%lld%lld%lld",&l1,&r1,&l2,&r2);
int k = T1.get(l2,r2) - T1.get(l1,r1);
if(k < 0){
k = -k;
swap(l1,l2);swap(r1,r2);
}
bool flag = true;
if(k % (r1 - l1 + 1) != 0) flag = false;
k /= (r1 - l1 + 1);
if(mod(T2.get(l1,r1) * power(p,k)) != T2.get(l2,r2)) flag = false;
if(flag) printf("YES\n");
else printf("NO\n");
}
}
return 0;
}
12.7
Luogu P6747 『MdOI R3』Teleport
题目分析:
显然可以考虑从高到低确定 \(k\) 的每一位。
我们考虑当前位选择 \(1\) 可行就是说我们剩下的 \(m\) 要大于等于当前位选 \(1\) 的贡献加上剩下位的最小贡献。
然后就根据这个贪心下去就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
int pre[100],cnt[100],a[N],pw[100];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=0; i<=50; i++){
for(int j=1; j<=n; j++){
cnt[i] += (a[j] >> i) & 1; //cnt[i] 记录第 i 位有多少个数为 1
}
}
pw[0] = 1;
for(int i=1; i<=50; i++) pw[i] = pw[i-1] * 2;
pre[0] = min(cnt[0],n-cnt[0]);
for(int i=1; i<=50; i++) pre[i] = pre[i-1] + min(cnt[i],n-cnt[i]) * pw[i];
//pre[i] 记录如果想要有解那么前 i 位至少需要多少的代价
int q;scanf("%lld",&q);
while(q--){
int m;scanf("%lld",&m);
int ans = 0;
bool flag = true;
for(int i=50; i>=0; i--){
if(m - ((n - cnt[i]) * pw[i]) >= (i == 0 ? 0 : pre[i-1])) ans += pw[i],m -= (n - cnt[i]) * pw[i];
else if(m - (cnt[i] * pw[i]) < (i == 0 ? 0 : pre[i-1])){
flag = false;break;
}
else m -= cnt[i] * pw[i];
}
if(!flag) ans = -1;
printf("%lld\n",ans);
}
return 0;
}
Luogu P6748 『MdOI R3』Fallen Lord
题目分析:
可以发现对于点 \(x\) 若其父亲为 \(y\),那么连接 \((x,y)\) 的这条边的值最优情况下只有三个值:\(a[x]、a[y]、m\)。
那么就可以显然设出来 \(dp[i][0/1/2]\) 代表以 \(i\) 为根的子树 \(i\) 与其父亲的边的权值为 \(a[i]/a[fa[i]]/m\) 的最大权值和。
对于转移就是从子树里面选几个去作为大于 \(i\) 的边转移就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int INF = 1e18+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt, to = _to;
}
}e[2 * N];
int n,m,cnt,head[N],deg[N],dp[N][3],a[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
dp[now][0] = a[now];dp[now][1] = a[fath];dp[now][2] = m;
vector<int> v;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
if(m <= a[now]){
dp[now][0] += max(dp[to][0],max(dp[to][1],dp[to][2]));
dp[now][1] += max(dp[to][0],max(dp[to][1],dp[to][2]));
dp[now][2] += max(dp[to][0],max(dp[to][1],dp[to][2]));
}
else if(a[to] <= a[now]){
dp[now][0] += max(dp[to][0],dp[to][1]);
dp[now][1] += max(dp[to][0],dp[to][1]);
dp[now][2] += max(dp[to][0],dp[to][1]);
v.push_back(dp[to][2]-max(dp[to][0],dp[to][1]));
}
else{
dp[now][0] += dp[to][1];
dp[now][1] += dp[to][1];
dp[now][2] += dp[to][1];
v.push_back(max(dp[to][0],dp[to][2]) - dp[to][1]);
}
}
sort(v.begin(),v.end());
int k = deg[now] - deg[now] / 2 - 1; //最多选择 k 个大于的
for(int i = v.size()-1; i>=0; i--){
if(v[i] > 0 && k > 0){
dp[now][0] += v[i];--k;
}
}
k = deg[now] - deg[now] / 2 - 1;
if(a[fath] > a[now]) k--;
if(k < 0) dp[now][1] = -INF;
else{
for(int i=v.size()-1; i>=0; i--){
if(v[i] > 0 && k > 0){
dp[now][1] += v[i];--k;
}
}
}
k = deg[now] - deg[now] / 2 - 1;
if(m > a[now]) k--;
if(k < 0) dp[now][2] = -INF;
else{
for(int i=v.size()-1; i>=0; i--){
if(v[i] > 0 && k > 0){
dp[now][2] += v[i];--k;
}
}
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld",&n,&m);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=1; i<n; i++){
int from,to;scanf("%lld%lld",&from,&to);
add_edge(from,to);add_edge(to,from);
deg[from]++;deg[to]++;
}
dfs(1,0);
printf("%lld\n",dp[1][0] - a[1]);
return 0;
}
Luogu P6786 「SWTR-6」GCDs & LCMs
题目分析:
通过打表可以显然地发现一个规律,只有 \((2 \times x,3 \times x)\) 才可能是满足条件的。
也就是说每当我们选择一个 \(3 \times x\) 就可以选择一个 \(2 \times x\)。
所以就可以将数离散化一下,就可以直接设 \(dp[i]\) 表示前 \(i\) 个数其中第 \(i\) 个必选的最大和,转移就很简单了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
int dp[N],a[N];
map<int,int> mp,pos;
signed main(){
int n;scanf("%lld",&n);
for(int i=1; i<=n; i++){
scanf("%lld",&a[i]);
mp[a[i]]++;
}
sort(a+1,a+n+1);
int tot = unique(a+1,a+n+1) - a - 1;
for(int i=1; i<=tot; i++) pos[a[i]] = i;
int mx = 0;
for(int i=1; i<=tot; i++){
dp[i] = mp[a[i]] * a[i] + (a[i] % 3 == 0 ? dp[pos[a[i] / 3 * 2]] : 0);
mx = max(mx,dp[i]);
}
printf("%lld\n",mx);
return 0;
}
12.8
Luogu P6833 [Cnoi2020]雷雨
题目分析:
我们可以枚举两条路径的交点,假设交点为 \(X\),我们的答案就是 \(dis(O,X) + dis(X,A) + dis(X,B)\)。
但是这样可能只是我们认为 \(X\) 是交点但是 \(X\) 实际却不是的情况,这种情况我们发现一定不优,因为当我们枚举到真正的交点的时候答案一定比现在这个假的交点要优。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3+5;
const int INF = 1e18+5;
int n,m,a,b,c,dis_a[N*N],dis_b[N*N],dis_c[N*N],R[N][N];
bool vis[N * N];
int dx[] = {0,1,-1,0,0};
int dy[] = {0,0,0,1,-1};
int num(int x,int y){
return (x-1)*m+y;
}
void get_dis(int x,int y,int *dis){
priority_queue<pair<int,pair<int,int> > > q;
dis[num(x,y)] = R[x][y];q.push({-dis[num(x,y)],{x,y}});
while(!q.empty()){
int x = q.top().second.first,y=q.top().second.second;q.pop();
if(vis[num(x,y)]) continue;
vis[num(x,y)] = true;
for(int i=1; i<=4; i++){
int fx = x + dx[i],fy = y + dy[i];
if(fx >= 1 && fx <= n && fy >= 1 && fy <= m && dis[num(fx,fy)] > dis[num(x,y)] + R[fx][fy]){
dis[num(fx,fy)] = dis[num(x,y)] + R[fx][fy];
q.push({-dis[num(fx,fy)],{fx,fy}});
}
}
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld%lld%lld%lld",&n,&m,&a,&b,&c);
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
scanf("%lld",&R[i][j]);
}
}
memset(vis,false,sizeof(vis));memset(dis_a,0x3f,sizeof(dis_a));get_dis(1,a,dis_a);
memset(vis,false,sizeof(vis));memset(dis_b,0x3f,sizeof(dis_b));get_dis(n,b,dis_b);
memset(vis,false,sizeof(vis));memset(dis_c,0x3f,sizeof(dis_c));get_dis(n,c,dis_c);
int ans = INF;
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
int tmp = num(i,j);
ans = min(ans,dis_a[tmp] + dis_b[tmp] + dis_c[tmp] - 2 * R[i][j]);
}
}
printf("%lld\n",ans);
return 0;
}
Luogu P6834 [Cnoi2020]梦原
题目分析:
考虑假设确定了树的形态那么我们的答案是什么:
因为我们可以理解为选择父亲的时候顺手把这个节点也弄完,剩下的就要靠自己了。
这样的话我们发现对于每一个点它的贡献只与它的父亲节点有关,也就是当我们的点 \(x\) 选择的父亲为 \(y\) 时对答案的贡献就是:
直接暴力复杂度是 \(O(nk)\) 的,不能接受。
但是这个式子显然可以考虑使用数据结构优化,就是维护小于等于 \(a[x]\) 的符合条件的父节点的个数和权值和,也就是开一个权值树状数组,以 \(a[x]\) 为下标,维护所有父节点的信息。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
const int N = 2e6+5;
int a[N],b[N],n,k,tot;
int mod(int x){
return (x % MOD+MOD)%MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = mod(res * a);
a = mod(a * a);
b >>= 1;
}
return res;
}
struct BIT{
int sum[N];
void modify(int x,int val){
for(;x <= n;x += x & (-x)) sum[x] = mod(sum[x] + val);
}
int query(int x){
int ans = 0;
for(;x; x -= x & (-x)) ans = mod(ans + sum[x]);
return ans;
}
}T1,T2;
//T1 维护数量,T2 维护权值和
signed main(){
scanf("%lld%lld",&n,&k);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]),b[++tot] = a[i];
sort(b+1,b+tot+1);tot=unique(b+1,b+tot+1)-b-1;
for(int i=1; i<=n; i++) a[i] = lower_bound(b+1,b+tot+1,a[i]) - b;
int ans = 0;
for(int i=1; i<=n; i++){
int tmp = i - max(i-k,1ll);
int sum1 = T1.query(a[i]),sum2 = T2.query(a[i]);
ans = mod(ans + mod(sum1 * b[a[i]] - sum2) * power(tmp,MOD-2));
if(i - k >= 1) T1.modify(a[i-k],-1),T2.modify(a[i-k],-b[a[i-k]]);
T1.modify(a[i],1);T2.modify(a[i],b[a[i]]);
}
ans = mod(ans + b[a[1]]); //不要忘了这一个啊
printf("%lld\n",ans);
return 0;
}
12.9
Luogu P6852 Mex
题目分析:
可以从小到大考虑每一个值。
对于值 \(x\) 其能出现的位置就是所有 \(Mex > x\) 的区间的交集去掉 \(Mex = x\) 的区间的并集,然后维护一下随意找个位置把这个数插上就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int L[N],R[N],suf_l[N],suf_r[N],ans[N],s[N];
bool vis[N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=0; i<=n; i++) L[i] = n,R[i] = 0,suf_l[i] = 0,suf_r[i] = n;
for(int i=1; i<=m; i++){
int l,r,val;scanf("%d%d%d",&l,&r,&val);
if(val) suf_l[val-1] = max(suf_l[val-1],l),suf_r[val-1] = min(suf_r[val-1],r); //交集
else s[l]++,s[r+1]--; //因为除了 0,其余的一定可以形成一段连续的区间
L[val] = min(L[val],l),R[val] = max(R[val],r); //并集
}
for(int i=1; i<=n; i++) s[i] += s[i-1];
for(int i=n-1; i>=0; i--){
suf_l[i] = max(suf_l[i],suf_l[i+1]);suf_r[i] = min(suf_r[i],suf_r[i+1]);
if(suf_l[i] > suf_r[i]){printf("-1\n");return 0;}
}
stack<int> st;
for(int i=suf_l[0]; i<=suf_r[0]; i++){ // >0的交集 - 0的并集
if(!s[i]) st.push(i),vis[i] = true;
}
if(st.empty()){printf("-1\n");return 0;}
ans[st.top()] = 0;st.pop();
for(int i=suf_l[0]; i<=suf_r[0]; i++){
if(!vis[i]) st.push(i),vis[i] = true;
}
for(int i=1; i<=n; i++){
if(L[i] <= R[i]){ //交集 - 并集
for(int j = suf_l[i]; j < L[i] && j <= suf_r[i] && !vis[j]; j++) st.push(j),vis[j] = true;
for(int j = suf_r[i]; j > R[i] && j >= suf_l[i] && !vis[j]; j--) st.push(j),vis[j] = true;
}
if(st.empty() || (st.top() >= L[i] && st.top() <= R[i])){printf("-1");return 0;}
ans[st.top()] = i;st.pop();
for(int j = max(suf_l[i],L[i]); j <= suf_r[i] && !vis[j]; j++) st.push(j),vis[j] = true;
for(int j = min(suf_r[i],R[i]); j >= suf_l[i] && !vis[j]; j--) st.push(j),vis[j] = true;
}
for(int i=0; i<=n; i++) printf("%d ",ans[i]);
return 0;
}
Luogu P6858 深海少女与胖头鱼
题目分析:
显然可以考虑设 \(f[i][j]\) 表示 \(i\) 条带圣盾的胖头鱼和 \(j\) 条不带圣盾的胖头鱼期望多少次全部杀光。
转移就是:
发现我们如果可以快速处理出 \(f[i][1]\) 的值,那么我们就可以在 \(O(m)\) 的时间复杂度内解决这个问题。
写出来 \(f[i][1]\) 的转移就是:
解一下就是:
可以发现 \(f[i][0] = f[i-1][1]\),所以最终的递推式就是:
这就是一个等差数列求和的形式,就可以很快地求解了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
int mod(int x){
return x % MOD;
}
int power(int a,int b){
a = mod(a);
int res = 1;
while(b){
if(b & 1) res = mod(res * a);
a = mod(a * a);
b >>= 1;
}
return res;
}
int calc(int x){ //f[x][1] 的值
x = mod(x);
return mod(mod(mod(x * (x + 1)) * power(2,MOD-2)) + 2 * x + 1);
}
int f(int n,int m){
if(m == 1) return calc(n);
if(m == 0) return mod(calc(n-1) + 1);
return mod(mod(mod(mod(n) * power(n + m,MOD-2)) * calc(n + m - 1)) + mod(mod(mod(m) * power(n + m,MOD-2)) * f(n,m-1)) + 1);
}
signed main(){
int n,m;
scanf("%lld%lld",&n,&m);
printf("%lld\n",f(n,m));
return 0;
}
12.10
Luogu P6859 蝴蝶与花
题目分析:
我们找一个前缀的区间,使得这个区间的和大于等于 \(s\) 且区间的右端点最小,可以发现区间和只有 \(s\) 和 \(s+1\) 两种。
如果为 \(s\) 则这个区间就是答案,而如果为 \(s+1\) 考虑有没有什么办法可以调整到 \(s\)。
那么就是找到这个区间的做右端点的右边第一个等于 \(1\) 的位置,把区间的其中一个对应的端点移动到对应的位置,这样就是合法的。
需要特判一下 \(a[1] = 1\) 的情况,因为这种情况直接将左端点右移 \(1\) 位就好了。
为什么上述是正确的呢?因为我们发现在上述情况下区间左右端点的值都一定是 \(2\),所以将其中一个移动到 \(1\),因为区间也是整体移动的所以就可以正好将一个 \(2\) 变成 \(1\) 区间和减一。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ri register int
#define ll long long
#define N 2000010
int n,m,a[N],x,y,z,sum[N<<2],num[N<<2];char ch[5];
inline int rd(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x;
}
inline void build(int id,int l,int r){
if(l==r){sum[id]=a[l];num[id]=(a[l]==1);return;}
int mid=(l+r)>>1;
build(id<<1,l,mid),build(id<<1|1,mid+1,r);
sum[id]=sum[id<<1]+sum[id<<1|1];
num[id]=num[id<<1]+num[id<<1|1];
}
inline void update(int id,int l,int r,int des,int val){
if(l==r){sum[id]=val;num[id]=(val==1);return;}
int mid=(l+r)>>1;
if(des<=mid) update(id<<1,l,mid,des,val);
else update(id<<1|1,mid+1,r,des,val);
sum[id]=sum[id<<1]+sum[id<<1|1];
num[id]=num[id<<1]+num[id<<1|1];
}
inline int querysum(int id,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return sum[id];
int mid=(l+r)>>1,ans=0;
if(ql<=mid) ans+=querysum(id<<1,l,mid,ql,qr);
if(qr>mid) ans+=querysum(id<<1|1,mid+1,r,ql,qr);
return ans;
}
inline int querynum(int id,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return num[id];
int mid=(l+r)>>1,ans=0;
if(ql<=mid) ans+=querynum(id<<1,l,mid,ql,qr);
if(qr>mid) ans+=querynum(id<<1|1,mid+1,r,ql,qr);
return ans;
}
inline int binary_querysum(int id,int l,int r,int val){
if(l>r) return 0;
int mid;
while(l<r){
mid=(l+r)>>1;
if(sum[id<<1]>=val) r=mid,id<<=1;
else l=mid+1,val-=sum[id<<1],id=(id<<1)|1;
}
return l;
}
inline int binary_querynum(int id,int l,int r,int val){
int mid;
while(l<r){
mid=(l+r)>>1;
if(num[id<<1]>=val) r=mid,id<<=1;
else l=mid+1,val-=num[id<<1],id=(id<<1)|1;
}
return l;
}
int main(){
n=rd(),m=rd();
for(ri i=1;i<=n;++i) a[i]=rd();a[n+1]=1;build(1,1,n+1);
while(m--){
scanf("%s",ch);
if(ch[0]=='A'){
x=rd();
if(x>sum[1]-1||x==0){puts("none");continue;}
int pos=binary_querysum(1,1,n+1,x);
if(querysum(1,1,n+1,1,pos)==x){printf("%d %d\n",1,pos);continue;}
if(a[1]==1){printf("%d %d\n",2,pos);continue;}
int lnxt=binary_querynum(1,1,n+1,1),rnxt=binary_querynum(1,1,n+1,querynum(1,1,n+1,1,pos)+1);
if(pos+lnxt-1>n&&rnxt>n){puts("none");continue;}
if(rnxt-pos+1<lnxt+1){
printf("%d %d\n",rnxt-pos+1,rnxt);
continue;
}
else{
printf("%d %d\n",lnxt+1,pos+lnxt-1);
continue;
}
//puts("none");
}
else{y=rd(),z=rd();a[y]=z;update(1,1,n+1,y,z);}
}
return 0;
}
Luogu P7043 「MCOI-03」村国
题目分析:
我们发现小 \(S\) 一定是在最优的点以及最优的点的最优的儿子之间反复横跳,那么推一下式子,算出来经过 \(M\) 天后最后的好感度就好了。
需要特判 \(n=1\) 的情况。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
int a[N];
signed main(){
int t;scanf("%lld",&t);
while(t--){
int n,m;
scanf("%lld%lld",&n,&m);
int mx = 0,tmp = 0;
for(int i=1; i<=n; i++){
scanf("%lld",&a[i]);
if(a[i] > a[mx] || (a[i] == a[mx] && i < mx)) mx = i;
}
for(int i=1; i<n; i++){
int from,to;scanf("%lld%lld",&from,&to);
if(from == mx && (a[to] > a[tmp] || (a[to] == a[tmp] && to < tmp))) tmp = to;
if(to == mx && (a[from] > a[tmp] || (a[from] == a[tmp] && from < tmp))) tmp = from;
}
int p = a[mx] - a[tmp];
if(p > m || tmp == 0) printf("%lld\n",mx);
else{
m -= p;a[tmp] = a[mx];
if(tmp > mx) a[mx] += m/2,a[tmp] += m - (m / 2);
else a[tmp] += m/2,a[mx] += m - (m / 2);
if(a[tmp] > a[mx] || (a[tmp] == a[mx] && tmp < mx)) printf("%lld\n",tmp);
else printf("%lld\n",mx);
}
}
return 0;
}
Luogu P7044 「MCOI-03」括号
题目分析:
可以发现这个最小操作次数就是没有匹配的括号数,这个 \(k\) 级偏值也显然就是来忽悠人的,一个组合数就可以解决了。
所以设 \(f[l][r]\) 表示 \(s[l,r]\) 的最小操作次数,\(G[i][j]\) 代表长度为 \(i\) 的区间分为 \(j\) 个连续的段,每一段的大小大于等于 \(0\) 的方案数,那么答案就是:
可以非常简单地维护出来
也就是说我们枚举左端点,维护 \(\sum_{r=i}^n G[n - r][k-1] \times f[l][r]\)。
设 \(suf[i] = \sum_{r=i}^n G[n-r][k-1]\) 可以发现若对于当前的左端点 \(l\) 其为左括号且与其匹配的右括号为 \(match[l]\),那么删除这个左端点的贡献就是:
也就是删除 \(l\) 会对所有的区间(不在匹配的位置之后)产生减一的贡献,而对于其匹配的位置及之后的区间会产生加一的贡献
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
const int MOD = 998244353;
char s[N];
int inv[N],fac[N],suf[N],match[N];
int mod(int x){
return (x % MOD + MOD) % MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = mod(res * a);
a = mod(a * a);
b >>= 1;
}
return res;
}
int C(int n,int m){
if(n < m) return 0;
return mod(fac[n] * mod(inv[m] * inv[n - m]));
}
signed main(){
int n,k;scanf("%lld%lld",&n,&k);
fac[0] = 1;
for(int i=1; i<=n+k; i++) fac[i] = mod(fac[i-1] * i);
inv[n + k] = power(fac[n+k],MOD-2);
for(int i = n+k-1; i>=0; i--) inv[i] = mod(inv[i+1] * (i+1));
scanf("%s",s+1);
stack<pair<char,int> > st;
int tmp = 0;
for(int i=1; i<=n; i++){
if(!st.empty() && s[i] == ')' && st.top().first == '(' ){
match[st.top().second] = i;st.pop();
}
else st.push({s[i],i});
tmp = mod(tmp + mod(1ll * st.size() * C(n - i + k - 1,k - 1)));
}
for(int i=1; i<=n; i++) suf[i] = C(n - i + k - 1,k-1);
for(int i=n; i>=1; i--) suf[i] = mod(suf[i] + suf[i + 1]);
int ans = 0;
for(int i=1; i<=n; i++){
ans = mod(ans + mod(C(i - 1 + k - 1,k-1) * tmp));tmp = mod(tmp - suf[i]);
if(match[i]) tmp = mod(tmp + 2 * suf[match[i]]);
}
printf("%lld\n",ans);
return 0;
}
12.11
Luogu P7108 移花接木
题目分析:
分情况讨论。
- \(a > b\)
会发现这种情况就只需要进行移花就可以了,设 \(sz\) 为树 \(b\) 的大小,那么答案显然就是:
- \(a = b\)
这种情况也类似直接把 \(h\) 层下面的全部去掉就可以了,答案就是:
- \(a < b\)
比较麻烦,既需要移花又接木。
移花肯定就是将 \(h\) 层下面挂着的全部去掉,但是我们可以拿这些去弥补我们需要接木的部分,可以发现肯定是可以补完的,所以答案就是:
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+7;
int mod(int x){
return x % MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = mod(res * a);
a = mod(a * a);
b >>= 1;
}
return res;
}
signed main(){
int T;scanf("%lld",&T);
while(T--){
int a,b,h;scanf("%lld%lld%lld",&a,&b,&h);
if(h == 0) printf("%lld\n",a);
else if(a > b){
int sz = 0;
if(b == 1) sz = h;
else sz = mod((power(b,h) - 1) * power(b-1,MOD-2));
int ans = mod(a * power(b,h)) + mod(sz * (a - b));
printf("%lld\n",mod(ans + MOD));
}
else if(a == b) printf("%lld\n",mod(a * power(b,h)));
else printf("%lld\n",mod(a * power(b,h)));
}
return 0;
}
Luogu P7109 滴水不漏
题目分析:
可以发现,如果我们在第 \(i\) 个桶的时候,前 \(i-1\) 个桶全部为空,那么我们就可以通过二分得到当前桶的水量。
然后就是清空第 \(i\) 个桶,也就是从最后开始倒水,倒满了就倒前面一个桶。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6+5;
int ans[N];
int pour(int x, int y){
int w;
printf("? %d %d\n", x, y);fflush(stdout);
scanf("%d", &w);
return w;
}
int cal(int i){ //二分得出 i 的剩余水量
int ans = 0,l = 1, r = i + 1;
while (l < r) {
int mid = (l + r)>>1;
if (pour(i, mid) == 0) r = mid;
else l = mid + 1;
pour(mid, i);
}
return l-1;
}
int main(){
int n;
scanf("%d", &n);
int pos = n;
for (int i = 1; i <= pos; i++){
int now = cal(i);
ans[i] += now;
while (i != pos) {
if (pour(i, pos) == 0){ //倒不满就 break,否则继续倒
ans[pos] -= now;
break;
}
int tmp = now;
now = cal(i); //原来 tmp 现在 now
ans[pos] += pos - (tmp - now); //pos - 倒进去的 = 原来的
pos--;
}
}
printf("!");
for (int i = 1; i <= n; i++) printf(" %d", ans[i]);
puts("");
return 0;
}
Luogu P7291 「EZEC-5」人赢 加强版
题目分析:
可以发现对于一个 \((i,k_i)\) 若存在 \((j,k_j)\) 满足 \(j > i\) 且 \(k_j \ge k_i\) 那么 \((i,k_i)\) 就一定不是最优的了。
所以就可以使用一个单调栈维护这个东西。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7+5;
int top,st[N],val[N];
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
signed main(){
int n;n=read();
int ans = 0;
for(int i=1; i<=n; i++){
int x;x=read();
while(top && x >= val[top]) ans = max(ans,val[top] * (i + st[top])),top--;
ans = max(ans,x * (st[top] + i));
st[++top] = i;val[top] = x;
}
printf("%lld\n",ans);
return 0;
}
12.13
[CQOI2011] 动态逆序对
题目分析:
可以考虑维护出每一个点对逆序对的贡献。
我们记 \(del[i]\) 为 \(i\) 的删除时间,\(pos[i]\) 为 \(i\) 的位置,\(val[i]\) 为 \(i\) 对应的数值。
那么其实就是求解这两种点的数量:
- \((del[j] > del[i]) \and (pos[j] > pos[i]) \and (val[j] < val[i])\)
- \((del[j] > del[i]) \and (pos[j] < pos[i]) \and (val[j] > val[i])\)
也就是经典的三维数点问题了,要注意没有被删除的数构成的逆序对不要算重了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
struct node{
int del,pos,val,tmp;
}a[N],b[N];
int res[N],ans[N],n,m,del[N],pos[N];
bool cmp_del(node a,node b){
if(a.del != b.del) return a.del > b.del;
if(a.pos != b.pos) return a.pos > b.pos;
return a.val < b.val;
}
void modify(int x,int val){
for(;x <= n; x += x & (-x)) res[x] += val;
}
int query(int x){
int ans = 0;
for(;x; x -= x & (-x)) ans += res[x];
return ans;
}
void cdq(int l,int r){
if(l == r) return;
int mid = (l + r)>>1;
cdq(l,mid);cdq(mid+1,r);
int now_l = l,now_r = mid+1,sz = l-1;
while(now_l <= mid && now_r <= r){
if(a[now_l].pos > a[now_r].pos) modify(a[now_l].val,1),b[++sz] = a[now_l++];
else ans[a[now_r].tmp] += query(a[now_r].val - 1),b[++sz] = a[now_r++];
}
while(now_r <= r) ans[a[now_r].tmp] += query(a[now_r].val - 1),b[++sz] = a[now_r++];
while(now_l <= mid) modify(a[now_l].val,1),b[++sz] = a[now_l++];
for(int i=l; i<=mid; i++) modify(a[i].val,-1);
for(int i=l; i<=r; i++) a[i] = b[i];
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld",&n,&m);
int sum = 0;
for(int i=1; i<=n; i++){
scanf("%lld",&a[i].val);
a[i].pos = i;a[i].tmp = i;
pos[a[i].val] = i;
a[i].del = m + 1;
}
for(int i=1; i<=m; i++){
scanf("%lld",&del[i]);
a[pos[del[i]]].del = i;
}
for(int i=1; i<=n; i++){
modify(a[i].val,1);
sum += i - query(a[i].val);
}
for(int i=1; i<=n; i++) modify(a[i].val, -1);
sort(a+1,a+n+1,cmp_del);
cdq(1,n);
for(int i=1; i<=n; i++){
a[i].val = (n - a[i].val + 1);
a[i].pos = (n - a[i].pos + 1);
}
sort(a+1,a+n+1,cmp_del);
cdq(1,n);
printf("%lld\n",sum);
for(int i=1; i<m; i++){
sum -= ans[pos[del[i]]];
printf("%lld\n",sum);
}
return 0;
}
[USACO18OPEN] Talent Show G
题目分析:
题目就是让我们选出一些牛使得 \(\sum w \ge W\) 的前提下 \(\dfrac{\sum t}{\sum w}\) 最大。
显然可以根据套路,二分一个答案 \(\lambda\),判断最值能否大于等于 \(\lambda\),就是经典地推式子了:
我们可以将 \(w\) 作为该物品的重量,\(t - \lambda \times w\) 作为该物品的价值,做一遍背包判断最优解是否大于等于 \(0\) 就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
const int INF = 1e9+5;
const double eps = 1e-5;
double dp[N],v[N];
int n,m,t[N],w[N];
bool check(double mid){
for(int i=1; i<=n; i++) v[i] = (double)t[i] - mid * w[i];
for(int i=1; i<=m; i++) dp[i] = -INF;
dp[0] = 0;
for(int i=1; i<=n; i++){
for(int j=m; j>=0; j--){
if(j + w[i] >= m) dp[m] = max(dp[m],dp[j] + v[i]);
else dp[j + w[i]] = max(dp[j + w[i]],dp[j] + v[i]);
}
}
return dp[m] >= 0;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d%d",&w[i],&t[i]);
double l = 0,r = 1000000;
while(r - l > eps){
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%d\n",int(l * 1000));
return 0;
}
[JSOI2016]最佳团体
题目分析:
经典题,即求 \(\dfrac{\sum P}{\sum S}\) 最大,可以考虑二分这个最大值,设为 \(\lambda\)。
稍微推导一下就可以得到,若 \(\dfrac{\sum P}{\sum S} \ge \lambda\) 也就意味着 \(\sum(P - \lambda \times S) \ge 0\)。
那么就将这个东西作为节点的权值,做一遍树上背包就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3+5;
const int INF = 1e9+5;
const double eps = 1e-4;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[2 * N];
int n,k,cnt,head[N],sz[N],p[N],s[N],fa[N];
double val[N],dp[N][N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void get(int now,int fath){
dp[now][1] = val[now];sz[now] = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
get(to,now);sz[now] += sz[to];
for(int j = sz[now]; j>=1; j--){
for(int t = 0; j - t >= 1 && t<=sz[to]; t++){
dp[now][j] = max(dp[now][j],dp[now][j-t] + dp[to][t]);
}
}
}
}
bool check(double mid){
for(int i=0; i<=n+1; i++) val[i] = (double)p[i] - mid * s[i];
for(int i=0; i<=n+1; i++){
for(int j=0; j<=k+1; j++){
dp[i][j] = -INF;
}
}
get(0,-1);
return dp[0][k+1] >= 0;
}
int main(){
scanf("%d%d",&k,&n);
for(int i=1; i<=n; i++){
scanf("%d%d%d",&s[i],&p[i],&fa[i]);
add_edge(fa[i],i);
}
double l = 0,r = 10000;
while(r - l > eps){
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.3f",l);
return 0;
}
[SDOI2017]新生舞会
题目分析:
可以考虑二分 \(C\) 的最优值,设为 \(\lambda\) 那么就是判断 \(\lambda\) 是否成立。
稍微推导一下发现,若 \(C \ge \lambda\) 成立当且仅当 \(\sum(a'_i - \lambda \times b'_i) \ge 0\) 成立。
那么就是一个最优匹配的问题,费用流就可以解决。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 100+5;
const int INF = 1e13+5;
const double eps = 1e-8;
struct edge{
int nxt,to,val;
double len;
edge(){}
edge(int _nxt,int _to,int _val,double _len){
nxt = _nxt,to = _to,val = _val,len = _len;
}
}e[2 * N * N];
int n,cnt = 1,s,t,a[N][N],b[N][N],head[N * N],cur[N * 5];
bool vis[N * 5],in_que[N * 5];
double dis[N * 5],val[N][N];
void add_edge(int from,int to,int val,double len){
e[++cnt] = edge(head[from],to,val,len);
head[from] = cnt;
e[++cnt] = edge(head[to],from,0,-len);
head[to] = cnt;
}
bool spfa(){
for(int i=1; i<=2*n+2; i++) dis[i] = -INF,cur[i] = head[i];
memset(in_que,false,sizeof(in_que));
int pre = dis[t];
queue<int> q;
q.push(s);dis[s] = 0;in_que[s] = true;
while(!q.empty()){
int now = q.front();q.pop();in_que[now] = false;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(e[i].val && dis[to] < dis[now] + e[i].len){
dis[to] = dis[now] + e[i].len;
if(!in_que[to]) q.push(to),in_que[to] = true;
}
}
}
return dis[t] != pre;
}
int dfs(int now,int limit){
if(now == t) return limit;
int flow = 0;vis[now] = true;
for(int i = cur[now]; flow < limit && i; i = e[i].nxt){
cur[now] = i;
int to = e[i].to;
if(!vis[to] && dis[to] == dis[now] + e[i].len && e[i].val){
int h = dfs(to,min(e[i].val,limit - flow));
e[i].val -= h;e[i^1].val += h;flow += h;
if(!h) dis[to] = -INF;
}
}
return flow;
}
double dinic(){
double ans = 0;
int flow;
while(spfa()){
memset(vis,false,sizeof(vis));
flow = dfs(s,INF);
ans += (double)dis[t] * flow;
}
return ans;
}
bool check(double mid){
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
val[i][j] = (double)a[i][j] - mid * b[i][j];
}
}
memset(head,0,sizeof(head));cnt = 1;
for(int i=1; i<=n; i++){
add_edge(s,i,1,0);add_edge(i+n,t,1,0);
for(int j=1; j<=n; j++){
add_edge(i,j+n,1,val[i][j]);
}
}
return dinic() >= 0;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld",&n);
s = 2 * n + 1,t = 2 * n + 2;
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
scanf("%lld",&a[i][j]);
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
scanf("%lld",&b[i][j]);
}
}
double l = 0,r = 10000;
while(r - l > eps){
double mid = (l + r)/2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.6f\n",l);
return 0;
}
12.14
Luogu P1527 [国家集训队]矩阵乘法
题目分析:
我们可以发现,我们可以做到以 \(O(n^2\log n)\) 的复杂度回答单次询问。
就是说我们每次二分一个值 \(x\),然后将整个矩阵里小于等于 \(x\) 的值的位置标记出来,然后求一个矩阵和然后继续二分。
但是我们发现这个预处理其实很不值,因为我们可能会使得某一种预处理在不同的询问中被处理多次。
所以就可以考虑整体二分,其实就是让所有的询问放在一起二分,这样一次预处理就可以解决对应的所有矩阵的询问。
下面就是理性分析一下复杂度了:
首先我们可以将所有位置按值排序,这样我们二分一个位置就好了,不需要二分值了。
总共二分有 \(\log n\) 层,每层预处理的复杂度是 \(O(n^2 \log^2n)\),因为需要维护一个二维树状数组,而且每一个位置显然最多只会被计算进贡献一次,对于每一个询问每一层复杂度就是 \(O(\log^2 n)\),所以总复杂度就是 \(O((n^2 + q) \log^3 n)\)
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 505;
const int Q = 5e5+5;
struct matrix{
int x,y,val;
}a[N*N];
struct Query{
int x1,y1,x2,y2,k;
void input(){
scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&k);
}
}query[Q];
int n,q,t1[Q],t2[Q],tot,ans[Q],id[Q],tmp[Q];
struct BIT{
int sum[N][N];
void modify(int x,int y,int val){
for(;x <= n; x += x & (-x)){
for(int fy = y; fy <= n; fy += fy & (-fy)){
sum[x][fy] += val;
}
}
}
int query(int x,int y){
int ans = 0;
for(;x;x -= x & (-x)){
for(int fy = y; fy; fy -= fy & (-fy)){
ans += sum[x][fy];
}
}
return ans;
}
int query(int x1,int y1,int x2,int y2){
return query(x2,y2) - query(x1-1,y2) - query(x2,y1-1) + query(x1-1,y1-1);
}
}bit;
bool cmp_val(matrix a,matrix b){
return a.val < b.val;
}
void solve(int l,int r,int ql,int qr){
if(ql > qr) return;
if(l == r){
for(int i=ql; i<=qr; i++) ans[id[i]] = a[l].val;
return;
}
int mid = (l + r)>>1,sz1 = 0,sz2 = 0;
for(int i=l; i<=mid; i++) bit.modify(a[i].x,a[i].y,1);
for(int i=ql; i<=qr; i++){
int now = id[i];
Query p = query[now];
int ans = bit.query(p.x1,p.y1,p.x2,p.y2);
if(ans >= p.k) t1[++sz1] = now;
else query[now].k -= ans,t2[++sz2] = now;
}
for(int i=l; i<=mid; i++) bit.modify(a[i].x,a[i].y,-1);
int sz = ql - 1;
for(int i=1; i<=sz1; i++) id[++sz] = t1[i];
for(int i=1; i<=sz2; i++) id[++sz] = t2[i];
solve(l,mid,ql,ql+sz1-1);solve(mid+1,r,ql+sz1,qr);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d",&n,&q);
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
int x;scanf("%d",&x);
a[++tot] = {i,j,x};
}
}
sort(a+1,a+tot+1,cmp_val);
for(int i=1; i<=q; i++){
query[i].input();
id[i] = i;
}
solve(1,n*n,1,q); //答案区间;询问区间
for(int i=1; i<=q; i++) printf("%d\n",ans[i]);
return 0;
}
CF601E A Museum Robbery
题目分析:
我们发现询问其实就是背包之后的结果,但是背包不支持删除那么怎么办呢?
每一个物品都有一个出现的区间,在这个区间里它出现。所以就可以考虑线段树分治,将每一个物品放到线段树上对应的区间上,显然这样的区间不会超过 \(\log n\) 个,然后采用永久标记的方法,每次到达这个节点就在原来背包的基础上将当前的这些物品加入到背包里,这样只需要记录没有操作之前的背包是什么就可以做到删除操作。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
const int base = 1e7+19;
const int MOD = 1e9+7;
struct node{
int l,r,v,w;
}E[N];
int n,k,tot,f[N];
vector<int> v[4 * N];
void modify(int now,int now_l,int now_r,int l,int r,int pos){
if(l <= now_l && r >= now_r){
v[now].push_back(pos);
return;
}
int mid = (now_l + now_r)>>1;
if(l <= mid) modify(now<<1,now_l,mid,l,r,pos);
if(r > mid) modify(now<<1|1,mid+1,now_r,l,r,pos);
}
void solve(int now,int now_l,int now_r,int *f){
int g[N];
for(int i=0; i<=k; i++) g[i] = f[i];
for(int p : v[now]){
for(int j=k; j>=E[p].w; j--){
g[j] = max(g[j],g[j - E[p].w] + E[p].v);
}
}
if(now_l == now_r){
int ans = 0;
for(int i = k; i>=1; i--) ans = (ans * base + g[i]) % MOD;
printf("%lld\n",ans);
return;
}
int mid = (now_l + now_r)>>1;
solve(now<<1,now_l,mid,g);solve(now<<1|1,mid+1,now_r,g);
}
signed main(){
int T = 1;
scanf("%lld%lld",&n,&k);
for(int i=1; i<=n; i++){
int v,w;scanf("%lld%lld",&v,&w);
E[++tot] = {1,-1,v,w};
}
int q;scanf("%lld",&q);
for(int i=1; i<=q; i++){
int opt;scanf("%lld",&opt);
if(opt == 1){
int v,w;scanf("%lld%lld",&v,&w);
E[++tot] = {T,-1,v,w};
}
else if(opt == 2){
int x;scanf("%lld",&x);
E[x].r = T-1;
}
else if(opt == 3){
++T;
}
}
--T;
for(int i=1; i<=tot; i++){
if(E[i].r == -1) E[i].r = T;
if(E[i].l <= E[i].r) modify(1,1,T,E[i].l,E[i].r,i);
}
solve(1,1,T,f);
return 0;
}
Luogu P5787 二分图 /【模板】线段树分治
题目分析:
考虑每一条边都有一个出现的区间,那么我们就大概率可以使用线段树分治使得我们的操作中没有删除操作,也就是只有加入操作。
只有加入操作就好办很多了,其实判断二分图最经典的做法就是二分图点染色,也就是将图中的点分为 \(S\) 和 \(T\) 两个集合,只要能保证任意一条边连接的两个点不在同一个集合内就好了。
这个显然可以使用并查集维护,也就是用两个节点 \(i\) 和 \(i+n\) 分别代表 \(i \in S\) 和 \(i \in T\) 的情况,那么一条边 \((u,v)\) 就是意味着当 \(u \in S \and v \in T\) 或 \(u \in T \and v \in S\),这样发现撤销其实也是很简单的,就是弄一个可撤销并查集就好了。所以问题就完美地解决了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 6e5+5;
struct node{
int x,fa,sz;
}st[N];
int n,m,k,top,sz[N],fa[N];
vector<PII> v[4 * N];
int find(int x){
if(fa[x] == x) return x;
return find(fa[x]);
}
void merge(int x,int y){
int fx = find(x),fy = find(y);
if(fx == fy) return;
if(sz[fx] < sz[fy]) swap(fx,fy);
st[++top] = {fy,fa[fy],sz[fy]};
fa[fy] = fx;sz[fx] += sz[fy];
}
void modify(int now,int now_l,int now_r,int l,int r,PII ed){
if(l <= now_l && r >= now_r){
v[now].push_back(ed);
return;
}
int mid = (now_l + now_r)>>1;
if(l <= mid) modify(now<<1,now_l,mid,l,r,ed);
if(r > mid) modify(now<<1|1,mid+1,now_r,l,r,ed);
}
void solve(int now,int now_l,int now_r){
bool flag = true;
int lst = top;
for(auto i : v[now]){
int a = find(i.first),b = find(i.second);
if(a == b){
for(int j=now_l; j<=now_r; j++) printf("No\n"); //小优化
flag = false;break;
}
merge(i.first,i.second + n);merge(i.second,i.first + n);
}
if(flag){
if(now_l == now_r) printf("Yes\n");
else{
int mid = (now_l + now_r)>>1;
solve(now<<1,now_l,mid);solve(now<<1|1,mid+1,now_r);
}
}
while(lst < top){
sz[fa[st[top].x]] -= st[top].sz;
fa[st[top].x] = st[top].fa;
--top;
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d%d",&n,&m,&k);
for(int i=1; i<=2*n; i++) fa[i] = i,sz[i] = 1;
for(int i=1; i<=m; i++){
int x,y,l,r;
scanf("%d%d%d%d",&x,&y,&l,&r);
modify(1,1,k,l+1,r,{x,y});
}
solve(1,1,k);
return 0;
}