KTT 小结
cheng2010
·
·
算法·理论
偶然看到,感觉挺牛逼,可以优化一些刁钻的 dp。
理论
考虑这样一个问题:
给定 k,b,x,每次给 k 或 b 加上一个数 a,查询 l,r,求 \displaystyle \max_{i=l}^{r}\{b_i+k_i x_i\}。
可以看到操作本质上是区间加上不同的数,只用调整那个 a。
用到 dp 上就是 f_{i}=\max(w1(i,j) f_j+w2(i,j)) 之类。
限制是必须加正数!否则时间复杂度无法保证。
KTT 本质就是线段树,每个节点存 kx+b 的 k,b,如果加就是 a 加上 ak。
然后,在线段树中我们要维护一个极其关键的值,\rm intr,我们称其为交替阈值。其中,交替是 KTT 一个重要的操作,他使得 KTT 可以处理最大值的询问,也给 KTT 解决的问题带来了限制。
交替:本问题与常规线段树问题最大的不同是,即便是对一整段区间修改同样的东西,这段区间的最大值也有可能改变,这就叫交替。我们不关注交替之后谁是最大值,因为我们可以直接 pushdown 加 pushup 计算出来,我们只关注什么时候会交替,这时候就需要交替阈值这个东西了。
线段树存的东西:k,b,lazy,intr,其中 intr 就是每次对 x 加上 k 时,它就减上 k,<0 时就该交替了。
可见,KTT 其实就是用阈值来剪枝。
接下来一个个函数来讲讲。
以下是一些前置,部分内容下面会说:
#include<bits/stdc++.h>
#define int long long
#define LL p<<1
#define RR p<<1|1
using namespace std;
const int N=1e6+7;
const int INF=1e18;
struct Line
{
int k,b;
inline bool operator < (const Line&B) const{return b==B.b?k<B.k:b<B.b;}
Line(){k=b=0;}
Line(int _a,int _b):k(_a),b(_b){};
friend inline int operator ^ (const Line&A,const Line&B)
{
Line X=A,Y=B;
if(X.k<Y.k||(X.k==Y.k&&X.b<Y.b)) swap(X,Y);
if(X.b>=Y.b) return INF;
return (Y.b-X.b)/(X.k-Y.k);
}
};
struct Tree
{
Line l;
int lazy,intr;
Tree(){lazy=l.k=l.b=intr=0;}
Tree(int _a,int _b,int _c,int _d):l(_a,_b),lazy(_c),intr(_d){};
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.l=max(A.l,B.l);
res.intr=min({A.intr,B.intr,A.l^B.l});
return res;
}
};
一、build
就是一些初始化。
inline void build(int p,int l,int r)
{
tr[p]=Tree(-INF,-INF,0,INF);
if(l==r) return;
int mid=l+r>>1;
build(LL,l,mid);
build(RR,mid+1,r);
}
二、pushdown
inline void push(int p,int x)
{
tr[p].lazy+=x;
tr[p].l.b+=x*tr[p].l.k;
tr[p].intr-=x;//距离交点更近一步。
}
inline void pushdown(int p)
{
int&x=tr[p].lazy;
push(LL,x),push(RR,x);
return x=0,void();
}
根据定义算即可。
三、pushup
其实就一句话。
tr[p]=tr[LL]+tr[RR];
就是前面的:
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.l=max(A.l,B.l);
res.intr=min({A.intr,B.intr,A.l^B.l});
return res;
}
重点在 Line 中的 ^ 运算。
friend inline int operator ^ (const Line&A,const Line&B)
{
Line X=A,Y=B;
if(X.k<Y.k||(X.k==Y.k&&X.b<Y.b)) swap(X,Y);
if(X.b>=Y.b) return INF;//平行或交点<=0则不会更替。
return (Y.b-X.b)/(X.k-Y.k);
}
就是求交点,因为一次函数是单调的,所以按交点讨论,和李超线段树差不多。
顺便提,我这种写法常数有点大,在上面的 ^ 运算中,可以写成一个函数,返回一个 pair,具体地,可以这样写:
inline pair<Line,int> merge(Line X,Line Y)
{
if(X.k<Y.k||(X.k==Y.k&&X.b<Y.b)) swap(X,Y);
if(X.b>=Y.b) return {X,INF};
return {Y,(Y.b-X.b)/(X.k-Y.k)};
}
这样求交点时顺便把最大值求了,常数自然减小。
不过个人认为重载好看一点,方便查错,而且除了 Ynoi 没人会卡你这个常数。
四、rebuild
KTT 所特有的函数,当 \text{intr} \le 0 时,最大值会改变,就要重构,其实就是遍历一遍然后 pushdown/up。
inline void rebuild(int p)
{
if(tr[p].intr>0) return;
pushdown(p);
rebuild(LL),rebuild(RR);
pushup(p);
return;
}
五、set
单点修改,没什么好说的。
inline void Set(int p,int l,int r,int x,Tree k)
{
if(l==r) return tr[p]=k,void();
int mid=l+r>>1;
pushdown(p);
if(mid>=x) Set(LL,l,mid,x,k);
else Set(RR,mid+1,r,x,k);
pushup(p);
}
六、update
区间改 k。
注意要 rebuild 以确保答案,然后就没了。
inline void update(int p,int l,int r,int L,int R,int x)
{
if(L<=l&&r<=R)
{
push(p,x);
rebuild(p);
return;
}
pushdown(p);
int mid=l+r>>1;
if(mid>=L) update(LL,l,mid,L,R,x);
if(mid<R) update(RR,mid+1,r,L,R,x);
pushup(p);
}
七、query
不必多说。
inline int query(int p,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return tr[p].l.b;
pushdown(p);
int mid=l+r>>1;
if(mid>=R) return query(LL,l,mid,L,R);
if(mid<L) return query(RR,mid+1,r,L,R);
return max(query(LL,l,mid,L,R),query(RR,mid+1,r,L,R));
}
拼起来就是模板题的代码啦。
:::success[code]
#include<bits/stdc++.h>
using namespace std;
namespace KTT
{
#define int long long
#define LL p<<1
#define RR p<<1|1
const int N=1e6+7;
const int INF=1e18;
struct Line
{
int k,b;
inline bool operator < (const Line&B) const{return b==B.b?k<B.k:b<B.b;}
Line(){k=b=0;}
Line(int _a,int _b):k(_a),b(_b){};
friend inline int operator ^ (const Line&A,const Line&B)
{
Line X=A,Y=B;
if(X.k<Y.k||(X.k==Y.k&&X.b<Y.b)) swap(X,Y);
if(X.b>=Y.b) return INF;
return (Y.b-X.b)/(X.k-Y.k);
}
};
struct Tree
{
Line l;
int lazy,intr;
Tree(){lazy=l.k=l.b=intr=0;}
Tree(int _a,int _b,int _c,int _d):l(_a,_b),lazy(_c),intr(_d){};
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.l=max(A.l,B.l);
res.intr=min({A.intr,B.intr,A.l^B.l});
return res;
}
}tr[N];
inline void push(int p,int x)
{
tr[p].lazy+=x;
tr[p].l.b+=x*tr[p].l.k;
tr[p].intr-=x;
}
inline void build(int p,int l,int r)
{
tr[p]=Tree(-INF,-INF,0,INF);
if(l==r) return;
int mid=l+r>>1;
build(LL,l,mid);
build(RR,mid+1,r);
}
inline void pushdown(int p)
{
int&x=tr[p].lazy;
push(LL,x),push(RR,x);
return x=0,void();
}
inline void pushup(int p)
{
tr[p]=tr[LL]+tr[RR];
}
inline void rebuild(int p)
{
if(tr[p].intr>0) return;
pushdown(p);
rebuild(LL),rebuild(RR);
pushup(p);
return;
}
inline void Set(int p,int l,int r,int x,Line k)
{
if(l==r) return tr[p].l=k,void();
int mid=l+r>>1;
pushdown(p);
if(mid>=x) Set(LL,l,mid,x,k);
else Set(RR,mid+1,r,x,k);
pushup(p);
}
inline void update(int p,int l,int r,int L,int R,int x)
{
if(L<=l&&r<=R)
{
push(p,x);
rebuild(p);
return;
}
pushdown(p);
int mid=l+r>>1;
if(mid>=L) update(LL,l,mid,L,R,x);
if(mid<R) update(RR,mid+1,r,L,R,x);
pushup(p);
}
inline int query(int p,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return tr[p].l.b;
pushdown(p);
int mid=l+r>>1;
if(mid>=R) return query(LL,l,mid,L,R);
if(mid<L) return query(RR,mid+1,r,L,R);
return max(query(LL,l,mid,L,R),query(RR,mid+1,r,L,R));
}
#undef int
#undef LL
#undef RR
}
int n,q;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>q;
KTT::build(1,1,n);
for(int i=1;i<=n;i++)
{
int k,b;
cin>>k>>b;
KTT::Set(1,1,n,i,{k,b});
}
while(q--)
{
int opt,l,r,x;
cin>>opt>>l>>r;
if(opt==1)
{
cin>>x;
KTT::update(1,1,n,l,r,x);
}
else
{
cout<<KTT::query(1,1,n,l,r)<<'\n';
}
}
}
:::
KTT 的时间复杂度分析非常困难,论文中说是 \log^3 的,不过可以比较一下,10^5 的数据量 KTT 110ms,树剖两只老哥 150ms,线段树一只老哥 80ms,所以只要不特意去卡就可以看作小常数 \log^2。
运用
KTT 还有神秘运用。
经典问题
考虑如下问题:
区间加,区间求最大子段和。
如果加的数是正数的话,我们就有 KTT 做法。否则就是什么分块套上闵可夫斯基和了。
因为求区间最大子段和在线段树上本质就是一堆取 \max,所以就是 KTT 可以做得东西。
重点就在这里。
```cpp
//lmax 前缀最大,rmax 后缀最大,sum 区间和,ans 区间最大子段和
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.lmax=max(A.lmax,A.sum+B.lmax);
res.rmax=max(B.rmax,A.rmax+B.sum);
res.sum=A.sum+B.sum;
res.ans=max({A.ans,B.ans,A.rmax+B.lmax});
res.intr=
min({
A.intr,
B.intr,
A.lmax^(A.sum+B.lmax),
B.rmax^(A.rmax+B.sum),
A.ans^B.ans,
max(A.ans,B.ans)^(A.rmax+B.lmax)
});//对每一对取 max 的地方求交点得最小值。
return res;
}
```
总的代码就很简单了。
:::success[code]
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e5+7;
const int INF=1e18;
namespace KTT
{
#define LL p<<1
#define RR p<<1|1
struct Line
{
int k,b;
inline bool operator < (const Line&B) const{return b==B.b?k<B.k:b<B.b;}
inline void add(int x){b+=k*x;}
Line(){k=b=0;}
Line(int _a,int _b):k(_a),b(_b){};
friend inline Line operator + (const Line&A,const Line&B) {return Line(A.k+B.k,A.b+B.b);}
friend inline int operator ^ (const Line&A,const Line&B)
{
Line X=A,Y=B;
if(X.k<Y.k||(X.k==Y.k&&X.b<Y.b)) swap(X,Y);
if(X.b>=Y.b) return INF;
return (Y.b-X.b)/(X.k-Y.k);
}
};
struct Tree
{
Line lmax,rmax,sum,ans;
int lazy,intr;
Tree(){lazy=intr=0;}
Tree(Line _lmax,Line _rmax,Line _sum,Line _ans,int _lazy,int _intr):lmax(_lmax),rmax(_rmax),sum(_sum),ans(_ans),lazy(_lazy),intr(_intr){};
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.lmax=max(A.lmax,A.sum+B.lmax);
res.rmax=max(B.rmax,A.rmax+B.sum);
res.sum=A.sum+B.sum;
res.ans=max({A.ans,B.ans,A.rmax+B.lmax});
res.intr=min({A.intr,B.intr,A.lmax^(A.sum+B.lmax),B.rmax^(A.rmax+B.sum),A.ans^B.ans,max(A.ans,B.ans)^(A.rmax+B.lmax)});
return res;
}
}tr[N<<2];
inline void push(int p,int x)
{
tr[p].lazy+=x;
tr[p].intr-=x;
tr[p].lmax.add(x);
tr[p].rmax.add(x);
tr[p].sum.add(x);
tr[p].ans.add(x);
}
inline void pushdown(int p)
{
int&x=tr[p].lazy;
push(LL,x),push(RR,x);
return x=0,void();
}
inline void pushup(int p)
{
tr[p]=tr[LL]+tr[RR];
}
inline void build(int p,int l,int r,int *A)
{
if(l==r)
{
auto v=Line(1,A[l]);
tr[p]=Tree(v,v,v,v,0,INF);
return;
}
int mid=l+r>>1;
build(LL,l,mid,A);
build(RR,mid+1,r,A);
pushup(p);
}
inline void rebuild(int p)
{
if(tr[p].intr>0) return;
pushdown(p);
rebuild(LL),rebuild(RR);
pushup(p);
return;
}
inline void update(int p,int l,int r,int L,int R,int x)
{
if(L<=l&&r<=R)
{
push(p,x);
rebuild(p);
return;
}
pushdown(p);
int mid=l+r>>1;
if(mid>=L) update(LL,l,mid,L,R,x);
if(mid<R) update(RR,mid+1,r,L,R,x);
pushup(p);
}
inline Tree query(int p,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return tr[p];
pushdown(p);
int mid=l+r>>1;
if(mid>=R) return query(LL,l,mid,L,R);
if(mid<L) return query(RR,mid+1,r,L,R);
return query(LL,l,mid,L,R)+query(RR,mid+1,r,L,R);
}
#undef LL
#undef RR
}
int n,q;
int a[N];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++) cin>>a[i];
KTT::build(1,1,n,a);
while(q--)
{
int opt,l,r,x;
cin>>opt>>l>>r;
if(opt==1)
{
cin>>x;
KTT::update(1,1,n,l,r,x);
}
else
{
cout<<max(0ll,KTT::query(1,1,n,l,r).ans.b)<<'\n';
}
}
}
```
:::
然后这道[毒瘤题](https://www.luogu.com.cn/problem/P6792)也差不多,只是将普通线段树换成了吉司机,核心在于如何用一次函数表达各个量,需要对 $k$ 的定义稍微改变一下,注意实现细节即可。
---
KTT 还有一大用处就是优化 DP。
~~感觉数据结构的最终奥义就是优化 DP。~~
还有些题目就要转换成一次函数的形式用 KTT 解答。
### 例一 [Innophone](https://www.luogu.com.cn/problem/P9288)
发现 $a,b$ 的最优值一定会在给定的 $x,y$ 中选择,否则可以找到一个更大的 $x$ 或 $y$ 赋值给 $a$ 或 $b$ 让答案更大。
考虑固定 $A,B$ 怎么做。对于某个 $a,b$,此时的答案为 $aA+bB$,其中 $A$ 代表 $x \ge a$ 的数量,$B$ 代表 $x<a \land y \ge b$ 的数量,是不是有点一次函数的味道了呢?
进一步,从小到大枚举 $a$,然后维护 $b$ 的最优解,因为是从小到大枚举,所以每次就是对一段区间的 $B$ 加上一,正中 KTT 下怀。注意要离散化,KTT 显然不支持动态开点。~~至少我不会。~~
:::success[参考代码]
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1.5e5+7;
namespace KTT
{
#define LL p<<1
#define RR p<<1|1
const int INF=1e18;
struct Line
{
int k,b;
inline bool operator < (const Line&B) const{return b==B.b?k<B.k:b<B.b;}
Line(){k=b=0;}
Line(int _a,int _b):k(_a),b(_b){};
friend inline int operator ^ (const Line&A,const Line&B)
{
int dk=A.k-B.k;
int db=B.b-A.b;
if((dk<=0&&db>=0)||(dk>=0&&db<=0)) return INF;
return (dk>0?(dk+db-1)/dk:(-dk-db-1)/(-dk));
}
};
struct Tree
{
Line l;
int lazy,intr;
Tree(){lazy=l.k=l.b=intr=0;}
Tree(int _a,int _b,int _c,int _d):l(_a,_b),lazy(_c),intr(_d){};
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.l=max(A.l,B.l);
res.intr=min({A.intr,B.intr,A.l^B.l});
return res;
}
}tr[N<<2];
inline void push(int p,int x)
{
tr[p].lazy+=x;
tr[p].l.b+=x*tr[p].l.k;
tr[p].intr-=x;
}
inline void build(int p,int l,int r)
{
tr[p]=Tree(-INF,-INF,0,INF);
if(l==r) return;
int mid=l+r>>1;
build(LL,l,mid);
build(RR,mid+1,r);
}
inline void pushdown(int p)
{
int&x=tr[p].lazy;
push(LL,x),push(RR,x);
return x=0,void();
}
inline void pushup(int p)
{
tr[p]=tr[LL]+tr[RR];
}
inline void rebuild(int p)
{
if(tr[p].intr>0) return;
pushdown(p);
rebuild(LL),rebuild(RR);
pushup(p);
return;
}
inline void Set(int p,int l,int r,int x,Line k)
{
if(l==r) return tr[p].l=k,void();
int mid=l+r>>1;
pushdown(p);
if(mid>=x) Set(LL,l,mid,x,k);
else Set(RR,mid+1,r,x,k);
pushup(p);
}
inline void update(int p,int l,int r,int L,int R,int x)
{
if(L<=l&&r<=R)
{
push(p,x);
rebuild(p);
return;
}
pushdown(p);
int mid=l+r>>1;
if(mid>=L) update(LL,l,mid,L,R,x);
if(mid<R) update(RR,mid+1,r,L,R,x);
pushup(p);
}
inline int query(int p,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return tr[p].l.b;
pushdown(p);
int mid=l+r>>1;
if(mid>=R) return query(LL,l,mid,L,R);
if(mid<L) return query(RR,mid+1,r,L,R);
return max(query(LL,l,mid,L,R),query(RR,mid+1,r,L,R));
}
#undef LL
#undef RR
}
int n;
struct Node
{
int x,y;
}a[N];
int ls[N],len;
signed main()
{
// ios::sync_with_stdio(0);
// cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].x>>a[i].y;
ls[i]=a[i].y;
}
sort(ls+1,ls+1+n);
len=unique(ls+1,ls+1+n)-ls-1;
sort(a+1,a+1+n,[](Node A,Node B){return A.x<B.x;});
a[0].x=-1;
KTT::build(1,1,len);
for(int i=1;i<=n;i++)
KTT::Set(1,1,len,lower_bound(ls+1,ls+1+len,a[i].y)-ls,{a[i].y,0});
int ans=0;
for(int i=1;i<=n;i++)
{
if(a[i].x==a[i-1].x)continue;
int j=i-1;
while(j>=1&&a[j].x==a[i-1].x)
{
KTT::update(1,1,len,1,lower_bound(ls+1,ls+1+len,a[j].y)-ls,1),j--;
}
ans=max(ans,KTT::query(1,1,len,1,len)+a[i].x*(n-i+1));
}
int j=n;
while(a[j].x==a[n].x)KTT::update(1,1,len,1,lower_bound(ls+1,ls+1+len,a[j].y)-ls,1),j--;
ans=max(ans,KTT::query(1,1,len,1,len));
cout<<ans;
}
```
:::
### 例二 [The Third Grace](https://www.luogu.com.cn/problem/CF1830F)
一眼 dp 的题。
考虑设 $f_i$ 为选了 $i$ 为最右边的点的最大贡献,则枚举上一个选了哪里,显然有转移:
$$
f_{i}=\max_{j<i} (f_j+p_j \sum [l\le j\le r\land r<i])
$$
在数轴上从小到大枚举,枚举到一个 $i$,就将右端点为 $i-1$ 的区间全部加一,无脑上 KTT 即可,多测超烦人!!
:::success[参考代码]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+7;
namespace KTT
{
#define LL p<<1
#define RR p<<1|1
const int INF=1e18;
struct Line
{
int k,b;
inline bool operator < (const Line&B) const{return b==B.b?k<B.k:b<B.b;}
Line(){k=b=0;}
Line(int _a,int _b):k(_a),b(_b){};
friend inline int operator ^ (const Line&A,const Line&B)
{
Line X=A,Y=B;
if(X.k<Y.k||(X.k==Y.k&&X.b<Y.b)) swap(X,Y);
if(X.b>=Y.b) return INF;
return (Y.b-X.b)/(X.k-Y.k);
}
};
struct Tree
{
Line l;
int lazy,intr;
Tree(){lazy=l.k=l.b=intr=0;}
Tree(int _a,int _b,int _c,int _d):l(_a,_b),lazy(_c),intr(_d){};
friend inline Tree operator + (Tree A,Tree B)
{
Tree res;
res.l=max(A.l,B.l);
res.intr=min({A.intr,B.intr,A.l^B.l});
return res;
}
}tr[N<<2];
inline void push(int p,int x)
{
tr[p].lazy+=x;
tr[p].l.b+=x*tr[p].l.k;
tr[p].intr-=x;
}
inline void build(int p,int l,int r)
{
tr[p]=Tree(-INF,-INF,0,INF);
if(l==r) return;
int mid=l+r>>1;
build(LL,l,mid);
build(RR,mid+1,r);
}
inline void pushdown(int p)
{
int&x=tr[p].lazy;
push(LL,x),push(RR,x);
return x=0,void();
}
inline void pushup(int p)
{
tr[p]=tr[LL]+tr[RR];
}
inline void rebuild(int p)
{
if(tr[p].intr>0) return;
pushdown(p);
rebuild(LL),rebuild(RR);
pushup(p);
return;
}
inline void Set(int p,int l,int r,int x,Line k)
{
if(l==r) return tr[p].l=k,void();
int mid=l+r>>1;
pushdown(p);
if(mid>=x) Set(LL,l,mid,x,k);
else Set(RR,mid+1,r,x,k);
pushup(p);
}
inline void update(int p,int l,int r,int L,int R,int x)
{
if(L<=l&&r<=R)
{
push(p,x);
rebuild(p);
return;
}
pushdown(p);
int mid=l+r>>1;
if(mid>=L) update(LL,l,mid,L,R,x);
if(mid<R) update(RR,mid+1,r,L,R,x);
pushup(p);
}
inline int query(int p,int l,int r,int L,int R)
{
if(L>R) return 0;
if(L<=l&&r<=R) return tr[p].l.b;
pushdown(p);
int mid=l+r>>1;
if(mid>=R) return query(LL,l,mid,L,R);
if(mid<L) return query(RR,mid+1,r,L,R);
return max(query(LL,l,mid,L,R),query(RR,mid+1,r,L,R));
}
#undef LL
#undef RR
}
int n,m;
vector<int> line[N];
int p[N],ans;
inline void solve()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int l,r;
cin>>l>>r;
line[r].push_back(l);
}
KTT::build(1,1,m);
for(int i=1;i<=m;i++) cin>>p[i];
for(int i=1;i<=m;i++)
{
for(auto v:line[i-1])
KTT::update(1,1,m,v,i-1,1);
int fi=KTT::query(1,1,m,1,i-1);
KTT::Set(1,1,m,i,{p[i],fi});
}
for(auto v:line[m])KTT::update(1,1,m,v,m,1);
cout<<KTT::query(1,1,m,1,m)<<'\n';
for(int i=1;i<=m;i++) line[i].clear();
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int T;
cin>>T;
while(T--) solve();
}
```
:::
## 小结
KTT 本质上就是线段树加上标记,做题时看看能不能转换为一次函数的形式。
学习就是要善用,巧用,不要无脑用,方能掌握,