1001
询问分治 dp
给一张图,点很少,边很多。每个询问给出一个区间,只能用边权在这个区间内的边,选一条路径,路径上边权严格递增,求最长路径(长度和权值是两个东西)
每一个询问,其实是个典,可以来一个O(m)O(m)O(m)的dp,扫一遍边转移即可得到最长路径。
但这里询问很多,肯定需要我们一块处理。并且每个询问还有边权的约束。
注意到对边权排序后,每个询问实际上是序列上的一个子区间dp的结果。这就可以转化成一个经典模型:对要进行dp的序列进行分治,每一层[L,R][L,R][L,R]中,处理所有询问区间[l,r][l,r][l,r]跨过[L,R][L,R][L,R]中点的询问(这些询问只能在这一层处理,更低层处理不了)。询问给的原始形式是边权的范围,我们需要二分找到可以包含这个边权范围的,最小的边权序列的区间,也就是转化成边权序列上的一个区间。
处理询问当然也不能对每个询问都一遍dp,注意到每个询问都是由[l,mid][mid+r][l,mid][mid+r][l,mid][mid+r]拼起来的,我们可以先预处理[L,mid][mid+1,R][L,mid][mid+1,R][L,mid][mid+1,R]的后缀和前缀dp结果,然后每个询问显然可以用一个[L,mid][L,mid][L,mid]的后缀和一个[mid+1,R][mid+1,R][mid+1,R]的前缀拼起来
这样也就是对于[L,mid][L,mid][L,mid]倒着dp,对[mid+1,R][mid+1,R][mid+1,R]正着dp。
回答询问需要先把每个询问[l,r][l,r][l,r]放在lll位置的桶里,然后遍历当前区间左半边[L,mid][L,mid][L,mid]的所有桶,看这些桶里的询问,有哪些是跨过中点的,就回答。
检查哪些询问是跨过中点的,不太能直接遍历每个桶里的所有询问,可能超时。可以对询问的右端点升序排序,然后从后面较大的rrr开始检查
PSPSPS:这玩意牛客寒假营似乎出过,我还补了,但还是没看出来。。
struct edge{
int u,v,w,d;
}e[N];
struct query{
int l,r;
}q[N];
int mx1[N][60],mx2[N][60];
void solve()
{
int n,m,Q;
cin>>n>>m>>Q;
rep(i,1,m){
cin>>e[i].u>>e[i].v;
cin>>e[i].w>>e[i].d;
}
sort(e+1,e+1+m,[](auto i,auto j){
return i.w<j.w;
});
vi w(m+1);
rep(i,1,m)w[i]=e[i].w;
vvi bin(m+1);
vi ans(Q+1);
rep(i,1,Q){
cin>>q[i].l>>q[i].r;
q[i].l=lower_bound(w.begin(),w.end(),q[i].l)-w.begin();
q[i].r=upper_bound(w.begin(),w.end(),q[i].r)-w.begin()-1;
// cout<<q[i].l<<' '<<q[i].r<<'\n';
if(q[i].l<=m){
bin[q[i].l].push_back(i);
}
}
rep(i,1,m){
sort(bin[i].begin(),bin[i].end(),[&](int i,int j){
return q[i].r<q[j].r;
});
}
auto &&work=[&](auto &&work,int L,int R)->void{
int cnt=0;
rep(i,L,R){
cnt+=bin[i].size();
}
if(!cnt)return;
if(L>R)return;
if(L==R){
for(int id:bin[L]){
if(q[id].r>=R)ans[id]=e[L].d;
}
return;
}
int mid=(L+R)>>1;
vvi f(n+1,vi(n+1,-1e18)),g(n+1,vi(n+1,-1e18));
rep(i,1,n){
f[i][i]=0;
g[i][i]=0;
}
rep(i,0,mid-L+1){
rep(x,1,n){
mx1[i][x]=0;
}
}
rep(i,0,R-mid){
rep(x,1,n){
mx2[i][x]=0;
}
}
int len=0;
for(int i=mid;i>=L;i--){
++len;
int u=e[i].u,v=e[i].v,d=e[i].d;
rep(x,1,n){
f[u][x]=max(f[u][x],f[v][x]+d);
mx1[len][x]=max(mx1[len-1][x],f[u][x]);
}
}
len=0;
rep(i,mid+1,R){
++len;
int u=e[i].u,v=e[i].v,d=e[i].d;
rep(x,1,n){
g[v][x]=max(g[v][x],g[u][x]+d);
mx2[len][x]=max(mx2[len-1][x],g[v][x]);
}
}
rep(i,L,mid){
while(bin[i].size()&&q[bin[i].back()].r>mid){
int id=bin[i].back();
int r=q[id].r;
int len1=mid-i+1;
int len2=r-mid;
// if(L==1&&R==5)cout<<id<<' '<<len1<<' '<<len2<<'\n';
rep(x,1,n){
ans[id]=max(ans[id],mx1[len1][x]+mx2[len2][x]);
}
bin[i].pop_back();
}
}
work(work,L,mid);
work(work,mid+1,R);
};
work(work,1,m);
rep(i,1,Q){
cout<<ans[i]<<'\n';
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int test=1;
cin>>test;
while(test--){
solve();
}
}
1004
思维 结论
全是结论,找到结论后很好写。
给两个序列,每次可以把一个长度k的子区间循环移位一次,问能否把两个序列变得完全相同?
首先显然必须两个序列的可重复集完全相同,也就是每种元素出现次数都相同。然如果k=1k=1k=1,无法进行操作,必须初始就相同。如果k=nk=nk=n只能整体循环,考虑最小表示法,也就是通过循环,把两个字符串都变成字典序最小的形式,再比较这两个形式是否相同。
PSPSPS:最小表示法其实是种思想,比较两个东西能否在某些操作后完全相同,考虑一个基准,把两个都变成这个基准下的,再来比较是否相同,在这里就是字符串变成字典序最小的形式,如果最小表示下相同,那说明两个东西都能变成这个最小表示,显然可以把一个变成最小表示,再从最小表示变成另一个,也就可以使两个东西相同
回到一般情况,如果k是偶数,我们总能交换相邻元素,手玩一下就能发现。所以就能对整个序列进行排序,肯定可以变成相同的,这是个重要结论。
然后k是奇数,通过归纳法(这块我也没懂),可以证明直到还剩两个元素,前面的都可以排序,所以就看最后这俩元素是不是相同的。
如果存在一种元素至少出现两次,我们可以把这种元素的两次出现放在最后,那a,b序列前面都排序了,完全相同,最后一种元素出现两次,ab也相同,那就可以全都相同。
如果没有一种元素出现至少两次,最后剩的这俩元素,可能是x,yx,yx,y也可能是y,xy,xy,x,要看a,b里这俩元素的顺序是否相同。但这显然不能模拟,太慢了。
注意到这两种情况的逆序对奇偶是相反的,并且k位的循环移位操作,相当于把一个元素进行k-1次相邻交换,每次相邻交换整个序列的逆序对奇偶反转,k是奇数,k-1是偶数,反转偶数次仍然不变,也就是k是奇数的话循环移位不改变整个序列的逆序对个数奇偶性。那么最后剩下这俩元素的逆序奇偶,和初始整个序列的逆序对奇偶是相同的。
我们想知道最后只剩两个元素未排序是的逆序对奇偶,直接对初始序列计算逆序对奇偶即可。也就是比较初始ab的逆序对奇偶是否相同。
总的来说,有这么几个结论
- k为偶数,k循环移位等价于可以交换任意相邻元素
- k=n,只能对整体循环移位,考虑最小表示法
- 序列中有元素出现超过1次,不论k奇偶都可以全部排序
- k为奇数,k循环移位不改变序列逆序对奇偶性
#include<bits/stdc++.h>
typedef long long ll;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;
inline int read() {
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
return x * f;
}
const int N = 1e5 + 10;
int n, k;
int a[N], A[N];
int b[N], B[N];
int s[N];
int find(int *a) {
rep(i, 0, n - 1) s[i] = a[i + 1];
int i = 0, j = 1, k = 0;
for(; i < n && j < n && k < n; )
if(s[(i + k) % n] == s[(j + k) % n])
k ++;
else {
s[(i + k) % n] > s[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
if (i == j) i++;
k = 0;
}
return min(i, j) + 1;
}
int c[N];
void add(int x) {for(; x <= n; x += (x & -x)) c[x] ++;}
int ask(int x) {int s = 0; for(; x; x -= (x & -x)) s += c[x]; return s;}
int work(int *a) {
int s = 0;
rep(i, 1, n) c[i] = 0;
per(i, n, 1) {
int x = lower_bound(A + 1, A + n + 1, a[i]) - A;
s ^= (ask(x) & 1);
add(x);
}
return s;
}
void solve() {
n = read(), k = read();
rep(i, 1, n) a[i] = A[i] = read();
rep(i, 1, n) b[i] = B[i] = read();
sort(A + 1, A + n + 1);
sort(B + 1, B + n + 1);
bool flag = true;
rep(i, 1, n) if(A[i] != B[i]) {flag = false; break;}
if(!flag) return puts("NO"), void();
if(n == k) {
int i = find(a);
int j = find(b);
bool flag = true;
rep(k, 1, n) {
int x = i + k - 1 > n ? i + k - 1 - n : i + k - 1;
int y = j + k - 1 > n ? j + k - 1 - n : j + k - 1;
if(a[x] != b[y]) {flag = false; break;}
}
return puts(flag ? "YES" : "NO"), void();
}
if(k == 1) {
bool flag = true;
rep(i, 1, n) if(a[i] != b[i]) {flag = false; break;}
return puts(flag ? "YES" : "NO"), void();
}
if(!(k&1)) return puts("YES"), void();
rep(i, 1, n - 1) if(A[i] == A[i + 1]) {puts("YES"); return;}
int i = work(a);
int j = work(b);
puts(i == j ? "YES" : "NO");
}
int main() {
int T = read(); while(T --) solve(); return 0;
}
1005
树的直径
树上问所有直径的公共边有哪些,有动态加点
如果没有加点,先找到一条直径,所有直径的公共边,一定是这条直径上的边的子集,并且,一定是这条直径上的一个子区间。具体来说,这个区间的左右端点满足这样的条件:从区间端点到直径端点的距离,和从区间端点不经过直径可到达的最大距离,一定相等,也就是直径可以在这里分叉,有至少两种路径都是直径,那么这个点分叉后的边,就都不是所有直径的公共边了。
那么可以把这颗树的形态转换成:在直径这条链上,所有直径的公共边是这条链形成的序列的一个区间,每个点可以长出一棵树,长出的树有一个深度。如果树的深度等于这一点到直径端点的距离,这个点就是直径的公共边区间的端点。
显然我们计算直径链上每个点出发,不走这条直径边,可以到达的最大深度,然后判断深度和这个点到直径端点的距离,即可判断出每个点是否是区间端点。
动态加点的话,有两种,一种是加在直径端点上了,那这条直径会比原本的其他直径都更长,成为新的,唯一的直径,不妨设加在直径右边了,那公共边区间的右端点,会移动新直径的最右边。
另一种是加在其他点上,也就是直径中间的点长出来的子树上,那对结果的影响就是,可能导致这个点所在子树的深度变大,也就是直径上对应点的最大深度变大,变大之后可能导致最大深度等于根到直径端点的距离,导致树根成为公共边区间的端点。
所以我们需要点对直径上的每个点,维护这个点长出的树的最大深度,并且为了加点后快速深度,需要记录每个点所在子树的根是直径上的哪个点,这是可以的,因为新加的点肯定和他的父亲的根是相同的,根据父亲记录即可,并且新加点的深度我们也要知道,才能更新子树最大深度,这也是容易的,也是因为父亲的深度我们可知,新加点的深度就是父亲深度+1
当然这题可做是因为保证了,无论何时,我们维护的这个直径一定都是直径,也就是不会出现在子树上加一个点,导致原本这条链不是直径了。
void solve()
{
int n,w,L=1,R=2;
cin>>n>>w;
vector<int>fa(n+10),sum(n+10),d(n+10);
fa[1]=1,fa[2]=2;
sum[1]=0,sum[2]=w;
int tot=2,len=2;
rep(i,1,n){
int op;
cin>>op;
if(op==1){
int w;
cin>>w;
++tot,++len;
R=len;
sum[len]=sum[len-1]+w;
fa[tot]=len;
d[tot]=0;
}
else if(op==2){
int x,w;
cin>>x>>w;
++tot;
fa[tot]=fa[x];
d[tot]=d[x]+w;
int pa=fa[tot];
if(sum[pa]==d[tot]){
L=max(L,pa);
}
if(sum[len]-sum[pa]==d[tot]){
R=min(R,pa);
}
}
else{
cout<<sum[R]-sum[L]<<'\n';
}
}
}
1006
多tag线段树/广义矩阵乘,动态dp
给一个环形序列,问选一个子序列,不能连续选超过三个,的最大元素和。带修改
先考虑不带修,显然就是一个状态机dp,环形+长度为3的打家劫舍,记录当前结尾连续了几个即可,只有4个状态(结尾连续了0/1/2/3个)
环的话,考虑两种思路,一种是我们可以在dp状态里增加一个维度,记录开头选中了连续几个,另一种思路是可以跑4个dp,每次钦定开头选中了iii个,进行特殊的初始化,对这四种情况取最值。
对于第一种做法,发现我们就是要维护前后缀,这其实可以线段树维护,维护最长全0子串就是类似的思路。对每个区间维护前后缀分别选了[0,3][0,3][0,3]个的最优答案,然后合并时dpdpdp转移即可。但这样转移时可能需要枚举左区间和右区间的前后缀长度,合并复杂度时m4=256m^4=256m4=256的,有点大,可能被卡常。如果优化转移,变成m3=64m^3=64m3=64才能过。赛时我写这个做法被卡到怀疑人生。。
对于第二种,由于我们枚举了开头选几个,dp状态里就不用保存开头了,这个dp就很简单,而且由于不是开头结尾的信息都维护,就不能线段树合并来进行dp转移了。
但这个dp本身就是个简单的状态机dp,只用到了加法和取max操作,还带修,可以考虑广义矩阵乘维护动态dp来做。也就是我们把矩阵运算的乘法和加法,改成加法和取max,然后就能用一个4∗44*44∗4矩阵表示这个状态机dp的转移方程,每个元素都会转移一次,也就是每个元素都有个转移矩阵,修改一个元素,就是修改这个矩阵的参数。
想知道所有元素的dp结果,由于矩阵也就是线性变换,是有结合率的,我们可以把状态逐步和所有矩阵相乘的转移过程,改成先让所有矩阵相乘,再把最终矩阵和状态向量乘起来。也就是我们要维护所有元素的转移矩阵乘起来的结果,这就是单点修,区间乘查询,可以线段树来做,我们把每个节点里增加一个矩阵,表示这个节点对应的区间的矩阵相乘的结果
为了防止被卡常,最好把矩阵用array实现,不要vector。并且最后所有元素的矩阵,可以左乘也可以右乘状态向量,但不同的方法,矩阵和状态向量都需要转置。
void print_Matrix(const Matrix &a){
int n=a.size();
int m=a[0].size();
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
cout<<a[i][j]<<' ';
}
cout<<'\n';
}
}
// 矩阵乘法
Matrix multiply(const Matrix &A, const Matrix &B, long long MOD=M2)
{
int n = A.size();
int m = B[0].size();
int k = B.size();
Matrix C;
rep(i,0,n-1){
rep(j,0,m-1){
C[i][j]=-1e9;
}
}
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
{
for (int l = 0; l < k; ++l)
{
C[i][j] = max(C[i][j] , A[i][l] + B[l][j] );
}
}
}
return C;
}
struct Tree
{
#define ls u << 1
#define rs u << 1 | 1
struct Node
{
int l, r;
Matrix m;
Node operator+(const Node &o)
{
Node res;
res.l = l;
res.r = o.r;
res.m=multiply(m,o.m);
return res;
}
} tr[N << 2];
void pushup(int u)
{
tr[u] = tr[ls] + tr[rs];
}
void build(int u, int l, int r,vi &a)
{
tr[u].l=l;
tr[u].r=r;
if (l == r){
tr[u].m={
{
// {0,0,0,0},
// {a[l],-M1,-M1,-M1},
// {-M1,a[l],-M1,-M1},
// {-M1,-M1,a[l],-M1}
{0,a[l],-M1,-M1},
{0,-M1,a[l],-M1},
{0,-M1,-M1,a[l]},
{0,-M1,-M1,-M1}
}
};
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid,a);
build(rs, mid + 1, r,a);
pushup(u);
}
void modify(int u, int idx,int val)
{
if (tr[u].l == tr[u].r)
{
tr[u].m={
{
// {0,0,0,0},
// {val,-M1,-M1,-M1},
// {-M1,val,-M1,-M1},
// {-M1,-M1,val,-M1}
{0,val,-M1,-M1},
{0,-M1,val,-M1},
{0,-M1,-M1,val},
{0,-M1,-M1,-M1}
}
};
return;
}
else
{
int mid = (tr[u].l + tr[u].r) >> 1;
if (mid >= idx)
modify(ls, idx, val);
else
modify(rs, idx, val);
pushup(u);
}
}
Node query(int u, int l, int r)
{
if (l <= tr[u].l && tr[u].r <= r)
return tr[u];
int mid = (tr[u].l + tr[u].r) >> 1;
if (r <= mid)
return query(ls, l, r);
if (l > mid)
return query(rs, l, r);
return query(ls, l, r) + query(rs, l, r);;
}
} t;
void solve()
{
int n,q;
cin>>n>>q;
vi a(n+1);
rep(i,1,n){
cin>>a[i];
}
t.build(1,1,n,a);
auto res=t.query(1,2,n);
Matrix ans1=res.m;
int ans=0;
ans=max(ans,max({ans1[0][0],ans1[0][1],ans1[0][2],ans1[0][3]}));
res=t.query(1,3,n);
ans1=res.m;
ans=max(ans,max({ans1[0][0],ans1[0][1],ans1[0][2]})+a[1]);
res=t.query(1,4,n);
ans1=res.m;
ans=max(ans,max({ans1[0][0],ans1[0][1]})+a[1]+a[2]);
res=t.query(1,5,n);
ans1=res.m;
ans=max(ans,max({ans1[0][0]})+a[1]+a[2]+a[3]);
cout<<ans<<'\n';
rep(i,1,q){
int x,v;
cin>>x>>v;
t.modify(1,x,v);
a[x]=v;
auto res=t.query(1,2,n);
Matrix ans1=res.m;
int ans=0;
ans=max(ans,max({ans1[0][0],ans1[0][1],ans1[0][2],ans1[0][3]}));
res=t.query(1,3,n);
ans1=res.m;
ans=max(ans,max({ans1[0][0],ans1[0][1],ans1[0][2]})+a[1]);
res=t.query(1,4,n);
ans1=res.m;
ans=max(ans,max({ans1[0][0],ans1[0][1]})+a[1]+a[2]);
res=t.query(1,5,n);
ans1=res.m;
ans=max(ans,max({ans1[0][0]})+a[1]+a[2]+a[3]);
cout<<ans<<'\n';
}
}
1010
动态维护树的直径
按权值顺序加点,每次加到一个权值,问树的半径长度?
动态加点,求树的直径,这里有结论:新增一个点c,原本的直径是ab,那么新的直径只会是ab,ac,bc中的一个,我们比较它们的长度即可,树上路径长度,需要lca。
对于他这个加点,每次加到一个权值,把点按照权值排序,然后双指针即可。
int lg[N],f[N][20],idx[N],d[N];
void solve()
{
int n=rd();
// cin>>n;
vi w(n+1);
rep(i,1,n)w[i]=rd();
vvi g(n+1);
rep(i,1,n-1){
int u=rd(),v=rd();
g[u].push_back(v);
g[v].push_back(u);
}
auto &&dfs=[&](auto &&dfs,int u,int fa)->void{
f[u][0]=fa;
rep(i,1,lg[d[u]]){
f[u][i]=f[f[u][i-1]][i-1];
}
for(int v:g[u]){
if(v==fa)continue;
d[v]=d[u]+1;
dfs(dfs,v,u);
}
};
auto lca=[&](int x, int y)->int
{
if (d[x] < d[y])
swap(x, y);
while (d[x] > d[y])
x = f[x][lg[d[x] - d[y]]];
if (x == y)
return x;
for (int i = lg[d[x]]; i >= 0; i--)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
};
auto dis=[&](int i,int j)->int{
int LCA=lca(i,j);
return d[i]+d[j]-2*d[LCA];
};
dfs(dfs,1,-1);
rep(i,1,n)idx[i]=i;
sort(idx+1,idx+1+n,[&](int i,int j){
return w[i]<w[j];
});
int a,b,dia=0;
rep(i,1,n){
if(w[i]==0){
a=b=i;
break;
}
}
int j=1;
rep(i,0,n-1){
while(j<=n&&w[idx[j]]<=i){
int x=idx[j];
int ax=dis(a,x);
int bx=dis(b,x);
if(ax>bx){
if(ax>dia){
dia=ax;
b=x;
}
}
else if(bx>dia){
dia=bx;
a=x;
}
j++;
}
// cout<<(dia+1)/2<<'\n';
printf("%d\n",(dia+1)/2);
}
}
signed main(){
// ios::sync_with_stdio(0);
// cin.tie(0),cout.tie(0);
int test=1;
rep(i,2,N-5){
lg[i]=lg[i>>1]+1;
}
// cin>>test;
test=rd();
while(test--){
solve();
}
}
1012
神秘dp
每次可以选一个区间,把元素变成区间最小值,操作任意次,问能得到多少种不同区间?
考虑每个区间的最值,可能会算重,不如考虑每个元素在哪个区间是最小值,然后每次选一个元素,把最小值是这个元素的区间都操作了。
dp(i,j)dp(i,j)dp(i,j)表示使用了前iii个元素作为变之后的最小值,也就是前i个元素作为最小值的区间都操作过了,前缀长度jjj的序列有多少种不同序列?
转移则是dp(i,j)=∑k=l−1jdp(i−1,k)dp(i,j)=\sum_{k=l-1}^{j} dp(i-1,k)dp(i,j)=∑k=l−1jdp(i−1,k),(l,r)(l,r)(l,r)是aia_iai作为最小值的区间,考虑我们可以规定[l,k][l,k][l,k]这一段都不变,k=l−1k=l-1k=l−1时就是对于aia_iai这个区间不进行任何操作。
iii这个维度可以滚动压掉,因为转移实际上就是前一轮的一个区间和,每轮做个前缀和即可。
void solve()
{
int n;
cin>>n;
vi nums(n+1);
rep(i,1,n)cin>>nums[i];
vector<int> l(n + 1, 0), r(n + 1, n + 1);
stack<int> s;
for (int i = 1; i <= n; i++)
{
while (s.size() && nums[i] <= nums[s.top()])
{
s.pop();
}
if (s.size())
l[i] = s.top();
s.push(i);
}
s = stack<int>();
for (int i = n; i >= 1; i--)
{
while (s.size() && nums[i] <= nums[s.top()])
{
s.pop();
}
if (s.size())
r[i] = s.top();
s.push(i);
}
vi f(n+2),pre(n+1);
f[0]=1,pre[0]=f[0];
rep(i,1,n){
pre[i]=pre[i-1]+f[i];
}
rep(i,1,n){
rep(j,l[i]+1,r[i]-1){
f[j]+=(pre[j-1]-(l[i]?pre[l[i]-1]:0)+M2)%M2;
f[j]%=M2;
}
rep(j,1,n){
pre[j]=pre[j-1]+f[j];
pre[j]%=M2;
// cout<<f[j]<<' ';
}
// cout<<'\n';
}
cout<<f[n]<<'\n';
}

51

被折叠的 条评论
为什么被折叠?



