zoukankan      html  css  js  c++  java
  • 树分治 复习总结

    前几天去填重建计划的坑的时候无意中发现自己的树分治一直有一个地方有点bug,是错误的QAQ(但是貌似影响不大

    于是皇德耀世,赶紧去找了几道树分治来练习一下

    bug是每次树分治找重心的时候sum直接用的w数组,可是对于每一层w数组要重新计算(然而我并没有QAQ

    WC2010 重建计划

    这是一份bug重重的代码,我用了19min写完,交上去1A

    可是我后来发现这份代码至少有三处错误QAQ数据也太弱了吧

    贴一份bug重重的代码吧,顺便锻炼找bug能力?

    做法是二分答案之后每条边-mid,判断条件是是否存在边权和>=0的路径

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #include<cstdlib>
    #define eps 1e-5
    using namespace std;
     
    const int maxn=100010;
    const int oo=0x7fffffff/3;
    int n,L,R,tmp;
    int u,v,b;
    int mn,mx;
    double ans,l,r;
    int h[maxn],cnt=0;
    struct edge{
        int to,next,w;
    }G[maxn<<1];
    int g,sum;
    int f[maxn],w[maxn];
    int son[maxn],tot=0;
    double fa[maxn];
    double dep[maxn],mx_dep[maxn];
    int Q[maxn];
    bool vis[maxn];
     
    bool cmp(const int &a,const int &b){
        return w[a]<w[b];
    }
    void add(int x,int y,int z){
        ++cnt;G[cnt].to=y;G[cnt].next=h[x];G[cnt].w=z;h[x]=cnt;
    }
    void read(int &num){
        num=0;char ch=getchar();
        while(ch<'!')ch=getchar();
        while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    }
    void cmax(int &a,int b){if(b>a)a=b;}
    void Get_G(int u,int fa){
        f[u]=0;w[u]=1;
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v]||v==fa)continue;
            Get_G(v,u);
            w[u]+=w[v];
            cmax(f[u],w[v]);
        }cmax(f[u],sum-w[u]);
        if(f[g]>f[u])g=u;
    }
    void Get_dis(int u,int f,int d,double dis,double k){
        if(d>tmp)tmp=d;
        if(dep[d]<dis)dep[d]=dis;
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v]||v==f)continue;
            Get_dis(v,u,d+1,dis+G[i].w-k,k);
        }return;
    }
    bool check(int u,double k){
        int len=0;tmp=0;
        for(int i=1;i<=w[u];++i)mx_dep[i]=-oo;
        for(int i=1;i<=tot;++i){
            int v=son[i];tmp=0;
            for(int j=1;j<=w[v];++j)dep[j]=-oo;
            Get_dis(v,-1,1,fa[v]-k,k);
            int now=0,h=0,t=-1;
            for(int j=tmp;j>=1;--j){
                if(R-j<0)continue;
                while(now<=R-j&&now<=len){
                    while(h<=t&&mx_dep[now]>mx_dep[Q[t]])t--;
                    Q[++t]=now;now++;
                }
                while(h<=t&&Q[h]<L-j)h++;
                if(h<=t&&mx_dep[Q[h]]+dep[j]>0)return true;
            }
            if(tmp>len)len=tmp;
            for(int i=1;i<=len;++i)mx_dep[i]=max(mx_dep[i],dep[i]);
        }return false;
    }
    void Get_ans(int u){
        l=ans;r=mx;
        tot=0;
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v])continue;
            son[++tot]=v;fa[v]=G[i].w;
        }
        sort(son+1,son+tot+1,cmp);
        while(r-l>eps){
            double mid=(l+r)/2.0;
            if(check(u,mid))l=mid;
            else r=mid;
        }ans=l;
    }
    void Get_div(int u){
        vis[u]=true;Get_ans(u);
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v])continue;
            sum=w[v];g=0;
            Get_G(v,-1);Get_div(g);
        }return;
    }
     
    int main(){
        scanf("%d%d%d",&n,&L,&R);
        mn=oo;mx=-oo;
        for(int i=1;i<n;++i){
            read(u);read(v);read(b);
            add(u,v,b);add(v,u,b);
            mx=max(mx,b);mn=min(mn,b);
        }ans=mn;
        f[0]=oo;g=0;sum=n;
        Get_G(1,-1);Get_div(g);
        printf("%.3lf
    ",ans);
        return 0;
    }
    

    我发现的三处bug有:

    1、w数组没有重新计算(清空mx_dep和dep也用的w数组,居然没有WA)

    2、i循环里又有一个i循环

    3、处理前没有对子树排序,容易被卡成O(n^2)

    SPOJ 免费旅行

    秀技术强行写O(nlogn)

    跟重建计划的做法是一样的,不过这是一份没有bug的代码

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #include<cstdlib>
    using namespace std;
    
    const int maxn=400010;
    const int oo=0x7fffffff/3;
    int n,k,m,tmp;
    int u,v,a;
    bool black[maxn];
    bool vis[maxn];
    int h[maxn],cnt=0;
    int ans=0;
    struct edge{
    	int to,next,w;
    }G[maxn<<1];
    int g,sum;
    int f[maxn],w[maxn],b[maxn];
    int son[maxn],tot=0;
    int fa[maxn],Q[maxn];
    int dep[maxn],mx_dep[maxn];
    
    bool cmp(const int &x,const int &y){
    	return b[x]<b[y];
    }
    void add(int x,int y,int z){
    	++cnt;G[cnt].to=y;G[cnt].next=h[x];G[cnt].w=z;h[x]=cnt;
    }
    void read(int &num){
    	num=0;int f=1;char ch=getchar();
    	while(ch<'!')ch=getchar();
    	if(ch=='-')f=-1,ch=getchar();
    	while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    	num*=f;
    }
    void cmax(int &a,int b){if(b>a)a=b;}
    void Get_G(int u,int fa){
    	f[u]=0;w[u]=1;b[u]=black[u];
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==fa)continue;
    		Get_G(v,u);
    		w[u]+=w[v];b[u]+=b[v];
    		cmax(f[u],w[v]);
    	}cmax(f[u],sum-w[u]);
    	if(f[g]>f[u])g=u;
    }
    void DFS(int u,int fa){
    	w[u]=1;b[u]=black[u];
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==fa)continue;
    		DFS(v,u);
    		w[u]+=w[v];b[u]+=b[v];
    	}return;
    }
    void Get_dis(int u,int f,int d,int dis){
    	if(d>tmp)tmp=d;
    	if(dep[d]<dis)dep[d]=dis;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==f)continue;
    		Get_dis(v,u,d+black[v],dis+G[i].w);
    	}return;
    }
    void Get_ans(int u){
    	int len=0,flag=black[u];
    	for(int i=0;i<=b[u];++i)mx_dep[i]=-oo;
    	mx_dep[0]=0;
    	for(int i=1;i<=tot;++i){
    		int v=son[i];tmp=0;
    		for(int j=0;j<=b[v];++j)dep[j]=-oo;
    		Get_dis(v,-1,black[v],fa[v]);
    		int now=0,mx=-oo;
    		for(int j=tmp;j>=0;--j){
    			if(k-flag-j<0)continue;
    			while(now<=len&&now<=k-flag-j){
    				mx=max(mx,mx_dep[now]);
    				now++;
    			}
    			ans=max(ans,dep[j]+mx);
    		}
    		if(tmp>len)len=tmp;
    		for(int j=len;j>=0;--j){
    			mx_dep[j]=max(mx_dep[j],dep[j]);
    		}
    	}
    	return;
    }
    void Get_div(int u){
    	DFS(u,-1);
    	vis[u]=true;tot=0;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v])continue;
    		son[++tot]=v;fa[v]=G[i].w;
    	}
    	sort(son+1,son+tot+1,cmp);
    	Get_ans(u);
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v])continue;
    		g=0;sum=w[v];
    		Get_G(v,-1);Get_div(g);
    	}return;
    }
    
    int main(){
    	int __size__=128<<20;
    	char *__p__=(char*)malloc(__size__)+__size__;
    	__asm__("movl %0, %%esp
    "::"r"(__p__));
    	read(n);read(k);read(m);
    	for(int i=1;i<=m;++i){
    		read(u);
    		black[u]=true;
    	}
    	for(int i=1;i<n;++i){
    		read(u);read(v);read(a);
    		add(u,v,a);add(v,u,a);
    	}
    	g=0;sum=n;f[0]=oo;
    	Get_G(1,-1);Get_div(g);
    	printf("%d
    ",ans);
    	return 0;
    }
    

    codeforces 190 div1 C

    一道构造题目,要求你给树上每个点赋上A-Z任意一个字母

    要求如果有两个节点的字母相同,则这两点间的路径上一定要有一个节点的字母比这两个节点的字母大

    定义A>B>C……

    其实这道题目的impossible是逗你玩的,不可能无解

    我的构造方式就是采用树分治,树分治的结构最多有logn层,第一层赋值A,第二层赋值B……

    不难发现这样赋值的正确性,因为当每一层的赋值之后每棵子树要赋值的数都比当前小,所以子树是独立的

    恰好满足树分治的性质

    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    
    const int oo=0x7fffffff/3;
    const int maxn=100010;
    int n,u,v;
    int h[maxn],cnt=0;
    struct edge{
    	int to,next;
    }G[maxn<<1];
    int g,sum;
    int f[maxn],w[maxn];
    char ans[maxn];
    bool vis[maxn];
    
    void add(int x,int y){
    	++cnt;G[cnt].to=y;G[cnt].next=h[x];h[x]=cnt;
    }
    void read(int &num){
    	num=0;char ch=getchar();
    	while(ch<'!')ch=getchar();
    	while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    }
    void cmax(int &a,int b){if(b>a)a=b;}
    void Get_G(int u,int fa){
    	f[u]=0;w[u]=1;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==fa)continue;
    		Get_G(v,u);w[u]+=w[v];
    		cmax(f[u],w[v]);
    	}cmax(f[u],sum-w[u]);
    	if(f[g]>f[u])g=u;
    }
    void DFS(int u,int f){
    	w[u]=1;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==f)continue;
    		DFS(v,u);w[u]+=w[v];
    	}return;
    }
    void Get_div(int u,int d){
    	vis[u]=true;DFS(u,-1);
    	ans[u]=d+'A';
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v])continue;
    		g=0;sum=w[v];
    		Get_G(v,-1);Get_div(g,d+1);
    	}return;
    }
    
    int main(){
    	read(n);
    	for(int i=1;i<n;++i){
    		read(u);read(v);
    		add(u,v);add(v,u);
    	}
    	f[0]=oo;g=0;sum=n;
    	Get_G(1,-1);Get_div(g,0);
    	for(int i=1;i<=n;++i){
    		putchar(ans[i]);
    		if(i==n)printf("
    ");
    		else printf(" ");
    	}return 0;
    }
    

    Uva Live 7148

    给定一棵树,每个点有点权

    要求你求树上的一条单调不降路径,且最大值和最小值的差不超过D

    输出这条路径的最大长度

    本来想强行写O(nlogn)的,挣扎了一会没有写出来

    就只好写O(nlog^2n)了,树分治的过程中维护一棵线段树之后分情况讨论即可

    每次完整查询一棵子树,在完整插入一棵子树

    线段树以权值为键值,保留当前权值的最大长度即可

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    
    const int maxn=100010;
    const int oo=0x7fffffff/3;
    int T,n,m,D,ans,mx;
    int u,v,kase,now;
    int c[maxn];
    int h[maxn],cnt=0;
    struct edge{
    	int to,next;
    }G[maxn<<1];
    int g,sum,tim;
    bool vis[maxn];
    int f[maxn],w[maxn];
    int MX[maxn<<2];
    int t[maxn<<2];
    
    void add(int x,int y){
    	++cnt;G[cnt].to=y;G[cnt].next=h[x];h[x]=cnt;
    }
    void read(int &num){
    	num=0;char ch=getchar();
    	while(ch<'!')ch=getchar();
    	while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    }
    void cmax(int &a,int b){if(b>a)a=b;}
    void UPD(int o,int L,int R,int p,int v){
    	if(L==R){
    		if(t[o]!=tim){t[o]=tim;MX[o]=v;}
    		else cmax(MX[o],v);
    		return;
    	}
    	int mid=(L+R)>>1;
    	int l=(o<<1),r=(l|1);
    	if(p<=mid)UPD(l,L,mid,p,v);
    	else UPD(r,mid+1,R,p,v);
    	t[o]=tim;
    	int A=(t[l]==tim?MX[l]:0);
    	int B=(t[r]==tim?MX[r]:0);
    	MX[o]=max(A,B);
    }
    int ask(int o,int L,int R,int x,int y){
    	if(t[o]!=tim)return 0;
    	if(L>=x&&R<=y)return MX[o];
    	int mid=(L+R)>>1;
    	if(y<=mid)return ask(o<<1,L,mid,x,y);
    	else if(x>mid)return ask(o<<1|1,mid+1,R,x,y);
    	else return max(ask(o<<1,L,mid,x,y),ask(o<<1|1,mid+1,R,x,y));
    	
    }
    void Get_G(int u,int fa){
    	f[u]=0;w[u]=1;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==fa)continue;
    		Get_G(v,u);w[u]+=w[v];
    		cmax(f[u],w[v]);
    	}cmax(f[u],sum-w[u]);
    	if(f[g]>f[u])g=u;
    }
    void DFS(int u,int f){
    	w[u]=1;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v]||v==f)continue;
    		DFS(v,u);w[u]+=w[v];
    	}return;
    }
    void Get_up(int u,int f,int d){
    	int tmp=ask(1,1,mx,max(c[u]-D,1),c[u]);
    	cmax(ans,tmp+d+1);
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(v==f||vis[v])continue;
    		if(c[v]>=c[u]&&c[v]-now<=D)Get_up(v,u,d+1);
    	}return;
    }
    void Get_down(int u,int f,int d){
    	int tmp=ask(1,1,mx,c[u],min(mx,c[u]+D));
    	cmax(ans,tmp+d+1);
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(v==f||vis[v])continue;
    		if(c[v]<=c[u]&&now-c[v]<=D)Get_down(v,u,d+1);
    	}return;
    }
    void DFS_down(int u,int f,int d){
    	UPD(1,1,mx,c[u],d);
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(v==f||vis[v])continue;
    		if(c[v]<=c[u]&&now-c[v]<=D)DFS_down(v,u,d+1);
    	}return;
    }
    void DFS_up(int u,int f,int d){
    	UPD(1,1,mx,c[u],d);
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(v==f||vis[v])continue;
    		if(c[v]>=c[u]&&c[v]-now<=D)DFS_up(v,u,d+1);
    	}return;
    }
    void Get_div(int u){
    	vis[u]=true;DFS(u,-1);
    	tim++;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v])continue;
    		if(c[v]>=c[u]&&c[v]-c[u]<=D)now=c[u],Get_up(v,-1,1);
    		if(c[v]<=c[u]&&c[u]-c[v]<=D)now=c[u],DFS_down(v,-1,1);
    	}
    	tim++;
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v])continue;
    		if(c[v]<=c[u]&&c[u]-c[v]<=D)now=c[u],Get_down(v,-1,1);
    		if(c[v]>=c[u]&&c[v]-c[u]<=D)now=c[u],DFS_up(v,-1,1);
    	}
    	for(int i=h[u];i;i=G[i].next){
    		int v=G[i].to;
    		if(vis[v])continue;
    		g=0;sum=w[v];
    		Get_G(v,-1);Get_div(g);
    	}return;
    }
    
    int main(){
    	//freopen("s.in","r",stdin);
    	//freopen("s1.out","w",stdout);
    	read(T);
    	while(T--){
    		read(n);read(D);mx=0;kase++;
    		for(int i=1;i<=n;++i)read(c[i]),mx=max(mx,c[i]);
    		memset(h,0,sizeof(h));cnt=0;
    		memset(vis,false,sizeof(vis));
    		for(int i=1;i<n;++i){
    			read(u);read(v);
    			add(u,v);add(v,u);
    		}
    		ans=1;g=0;sum=n;f[0]=oo;
    		Get_G(1,-1);Get_div(g);
    		printf("Case #%d: %d
    ",kase,ans);
    	}return 0;
    }
    

    清澄 A1486 树

    给定一棵树,树上有点权,每个点要么是黑点要么是白点

    求一条经过至少K个黑点的路径,最大化路径异或和

    如果没有K个黑点的限制,我们直接trie上跑一跑就可以了

    之后我们用树分治去掉K个黑点的限制,剩下的问题就是裸的trie了QAQ

    关键是查询的时候还要有黑点的限制

    貌似可以用可持久化trie搞一搞?

    我的做法是对于trie的节点额外维护一个mx域,表示子树扩展出来的叶节点们的黑点的最大值

    这样我们在trie上贪心判断一下就可以了QAQ

    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    const int oo=0x7fffffff/3;
    const int maxn=100010;
    int n,k,u,v,ans;
    int black[maxn],c[maxn];
    bool vis[maxn];
    int h[maxn],cnt=0;
    struct edge{
        int to,next;
    }G[maxn<<1];
    int g,tot,sum;
    int mx[5000010];
    int nxt[5000010][2];
    int f[maxn],w[maxn];
    void add(int x,int y){
        ++cnt;G[cnt].to=y;G[cnt].next=h[x];h[x]=cnt;
    }
    void read(int &num){
        num=0;char ch=getchar();
        while(ch<'!')ch=getchar();
        while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    }
    void cmax(int &a,int b){if(b>a)a=b;}
    void Get_G(int u,int fa){
        f[u]=0;w[u]=1;
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v]||v==fa)continue;
            Get_G(v,u);w[u]+=w[v];
            cmax(f[u],w[v]);
        }cmax(f[u],sum-w[u]);
        if(f[g]>f[u])g=u;
    }
    void DFS(int u,int f){
        w[u]=1;
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v]||v==f)continue;
            DFS(v,u);w[u]+=w[v];
        }return;
    }
    int Newnode(){
        tot++;nxt[tot][0]=nxt[tot][1]=0;
        mx[tot]=0;return tot;
    }
    void insert(int Num,int k){
        int now=1;
        for(int i=31;i>=0;--i){
            int id=(Num>>i&1);
            if(!nxt[now][id])nxt[now][id]=Newnode();
            now=nxt[now][id];mx[now]=max(mx[now],k);
        }return;
    }
    int ask(int Num,int k){
        int now=1,L=nxt[now][1],R=nxt[now][0];
        if(mx[L]<k&&mx[R]<k)return -1;
        int ans=0;
        for(int i=31;i>=0;--i){
            int id=(Num>>i&1);id^=1;
            if(nxt[now][id]&&mx[nxt[now][id]]>=k){
                ans|=(1<<i);
                now=nxt[now][id];
            }else now=nxt[now][id^1];
        }return ans;
    }
    void Get_ans(int u,int f,int d,int b){
        int tmp=ask(d,k-b);ans=max(ans,tmp);
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v]||v==f)continue;
            Get_ans(v,u,d^c[v],b+black[v]);
        }return;
    }
    void Get_insert(int u,int f,int d,int b){
        insert(d,b);
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v]||v==f)continue;
            Get_insert(v,u,d^c[v],b+black[v]);
        }return;
    }
    void Get_div(int u){
        tot=1;nxt[tot][0]=nxt[tot][1]=0;
        insert(0,0);
        if(black[u]>=k)ans=max(ans,c[u]);
        vis[u]=true;DFS(u,-1);
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v])continue;
            Get_ans(v,-1,c[u]^c[v],black[v]+black[u]);
            Get_insert(v,-1,c[v],black[v]);
        }
        for(int i=h[u];i;i=G[i].next){
            int v=G[i].to;
            if(vis[v])continue;
            g=0;sum=w[v];
            Get_G(v,-1);Get_div(g);
        }return;
    }
    int main(){
        read(n);read(k);
        for(int i=1;i<=n;++i)read(black[i]);
        for(int i=1;i<=n;++i)read(c[i]);
        for(int i=1;i<n;++i){
            read(u);read(v);
            add(u,v);add(v,u);
        }ans=-1;
        g=0;sum=n;f[0]=oo;
        Get_G(1,-1);Get_div(g);
        printf("%d
    ",ans);
        return 0;
    }
    

    树分治通常情况下是解决树上路径问题

    适用范围很广,可以用来最优化问题也可以用来计数

    也有一部分的树分治的问题利用的是重心的性质和树分治logn层的结构

    如果遇到树上路径相关问题不妨可以用树分治尝试一下

    树分治貌似可以兼容很多数据结构来维护信息?

    待完成的几道题目:

    两道树分治+FFT的

    还有一道 紫荆花之恋

    一些知识点的坑:

    回文相关练习

    可持久化trie

  • 相关阅读:
    用EasyDarwin进行IPTV rtsp mpeg-ts smil流的转发和分发直播服务
    EasyDarwin添加自定义的服务模块EasyMyModule
    EasyDarwin添加自定义的服务模块EasyMyModule
    EasyDarwin Streaming Server对Task的调用方法
    EasyDarwin Streaming Server对Task的调用方法
    开源G711A/PCMA、G711U/PCMU、G726、PCM转码AAC项目EasyAACEncoder
    开源G711A/PCMA、G711U/PCMU、G726、PCM转码AAC项目EasyAACEncoder
    EasyDarwin EasyClient开源流媒体播放器,支持多窗口显示
    EasyDarwin EasyClient开源流媒体播放器,支持多窗口显示
    解决用EasyDarwin开源流媒体服务器做HLS直播时Flash Player卡住的问题
  • 原文地址:https://www.cnblogs.com/joyouth/p/5555035.html
Copyright © 2011-2022 走看看