zoukankan      html  css  js  c++  java
  • 图的连通性专题及模板

    图的连通性

    dfn[u]: 表示节点u的搜索优先级
    low[u]: 表示节点u,通过其本身或其子节点能到达的最小有搜索优先级
    
    low[u] = Min{
        1. dfn[u]         其本身搜索优先级
        2. Min{ low[v] }  其子节点v能到达的最小优先级
        3. Min( dfn[v] )  其通过回边(u,v),其中v为u的祖先节点,的优先级
    }

    一 无向图

    1. 割点

    又名关键点,若删除该点与其发出的边.则整个图不连通.

    当前顶点u是一个关键点的充要条件是:

    1. 若顶点U是根,则其必定包含两个以上的子节点. (因为若只有一个.删除了U之后,图仍然连通)

    2. 若顶点U不是根, 则当 dfn[u] <= low[v] , (其中V是U的子孙节点). 因为V无法通过其本身或子孙到达U或者U更高级的点.所以删除U后,图不连通.

    poj 1523 SPF

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    
    const int N = 1010;
    
    int edge[N][N];
    int n, son;
    
    int subnet[N], dfn[N], low[N], tmpdfn;
    bool vis[N];
    
    void init(){
        tmpdfn = 1; son = 0;
        memset(dfn,0,sizeof(dfn));
        memset(low,0,sizeof(low));
        memset(vis,0,sizeof(vis));
        memset(subnet,0,sizeof(subnet));
        low[1] = dfn[1] = 1; vis[1] = true;
    }
    void dfs(int u){
       // printf("u = %d
    ", u );
        for(int v = 1; v <= n; v++)
        {
            if( edge[u][v] ){
                if( !vis[v] ){
                    vis[v] = true;
                    dfn[v] = low[v] = ++tmpdfn;
                    dfs( v );
                    low[u] = min( low[u], low[v] );
                    if( low[v] >= dfn[u] ){
                        if( u != 1 ) subnet[u]++;
                        else son++;
                    }
                }
                else low[u] = min( low[u], dfn[v] );
            }
        }
    }
    
    int main(){
      //  freopen("1.in","r",stdin);
        int Case = 0;
        while( 1 ){
            int u, v; n = 0;
            scanf("%d", &u);
            if( u == 0 ) break;
            memset(edge,0,sizeof(edge));
            scanf("%d",&v);
            if( u > n ) n = u;
            if( v > n ) n = v;
            edge[u][v] = edge[v][u] = 1;
            while(1){
                scanf("%d",&u);
                if( u == 0 ) break;
                scanf("%d",&v);
                if(u > n) n = u;
                if(v > n) n = v;
                edge[u][v] = edge[v][u] = 1;
            }
            if( Case ) puts("");
            printf("Network #%d
    ", ++Case);
            init();
            dfs(1);
            if( son > 1 ) subnet[1] = son-1;
            bool find = false;
            for(int i = 1; i <= n; i++)
            {
                if( subnet[i] ){
                    find = true;
                    printf("  SPF node %d leaves %d subnets
    ", i, subnet[i]+1);
                }
            }
            if( !find ) printf("  No SPF nodes
    ");
        }
        return 0;
    }
    View Code

    2. 割边(桥)

    割边的判定条件:  dfn[u] < low[v] .    割边要注意重边

    zoj 2588 burning bridge

    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    #include<map>
    #include<vector>
    #include<algorithm>
    using namespace std;
    
    const int N = (int)1e4+10;
    
    struct Edge{
        int v, tag, nxt;
    }edge[N*20];
    int head[N], idx;
    int n, m;
    int dfn[N], low[N], dep;
    bool vis[N];
    
    map< pair<int,int>,int  > mp;
    vector<int> S;
    
    void AddEdge(int u,int v)
    {
        for(int i = head[u]; ~i; i = edge[i].nxt )
        if( edge[i].v == v ){
            edge[i].tag++;  return;
        }
        edge[idx].v = v; edge[idx].nxt = head[u]; edge[idx].tag = 0;
        head[u] = idx++;
    }
    void input(){
        memset( head, -1, sizeof(head));
        idx = 0;
        scanf("%d%d", &n,&m);
        mp.clear();
        for(int i = 0; i < m; i++)
        {
            int u, v;
            scanf("%d%d", &u,&v);
            mp[ make_pair(u,v) ] = mp[ make_pair(v,u) ] = i+1;
            AddEdge(u,v), AddEdge(v,u);
        }
    }
    void tarjan(int u,int pre)
    {
        vis[u] = true;
        dfn[u] = low[u] = ++dep;
        for(int i = head[u]; ~i; i = edge[i].nxt )
        {
            int v = edge[i].v;
            if( v == pre ) continue;
            if( !vis[v] )
            {
                tarjan( v, u );
                low[u] = min( low[u], low[v] );
            }
            else low[u] = min( low[u], dfn[v] );
            if( (low[v] > dfn[u]) && !edge[i].tag ) S.push_back( mp[make_pair(u,v)] );
        }
    }
    void solve(){
        S.clear();
    
        memset(vis,0,sizeof(vis));
        dep = 0;
        tarjan(1,0);
    
        sort(S.begin(), S.end());
        int tot = (int)S.size();
        printf("%d
    ", tot);
        for(int i = 0; i < tot; i++)
        printf(i == 0 ? "%d" :" %d", S[i] );
        if(tot) puts("");
    }
    
    int main(){
       /// freopen("1.in","r",stdin);
        int _;
        scanf("%d", &_);
        for(int Case = 0; Case < _; Case++)
        {
            if(Case) puts("");
            input();
            solve();
        }
        return 0;
    }
    View Code

    3. 重连通分量(顶点重复,关键点)

    具体做法是,通过一个栈将所有经过的边存储起来.当顶点U的子节点满足 dfn[u] <= low[v] 时,证明顶点U为一个关键点.则顺带找出了一个重连通分量.

    要注意的是, 找的过程.所有的边只能被访问一次. 注意标记.

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    #include<stack>
    using namespace std;
    
    const int N = 10101;
    int n, m;
    
    struct Edge{
        int u, v, nxt;
        Edge(){}
        Edge(int _u,int _v){
            u = _u; v = _v; nxt = 0;
        }
        int cmp( Edge &t ){
            return (u==t.u&&v==t.v)||(u==t.v&&v==t.u);
        }
        void print(){
            printf("%d->%d ", u, v );
        }
    }edge[N<<4];
    int head[N], idx;
    int dfn[N], low[N], dep;
    bool vis[N];
    bool gao[N][N];
    
    stack< Edge > S;
    
    void AddEdge(int u,int v)
    {
        edge[idx].u = u; edge[idx].v = v;
        edge[idx].nxt = head[u];
        head[u] = idx++;
    }
    void input(){
        memset(head,-1,sizeof(head));
        idx = 0;
        for(int i = 0; i < m; i++)
        {
            int u , v;
            scanf("%d%d", &u,&v);
            AddEdge(u,v); AddEdge(v,u);
        }
    }
    void tarjan(int u,int pre)
    {
        dfn[u] = low[u] = ++dep;
        vis[u] = true;
        for(int i = head[u]; ~i; i = edge[i].nxt )
        {
            int v = edge[i].v;
            if( v == pre || gao[v][u] || gao[u][v] ) continue;
    
            gao[v][u] = gao[u][v] = true;    // 这里要注意标记边.回边也只能走一次.
            S.push( Edge(u,v) );
            if( !vis[v] ){
                tarjan(v,u);
                low[u] = min( low[u], low[v] );
            }
            else low[u] = min( low[u], dfn[v] ); // 回边
            if( low[v] >= dfn[u] ){ //删除顶点U,子节点V所在的子树将脱离.
                    Edge t = Edge(u,v);
                    while(1)
                    {
                        Edge t1 = S.top(); S.pop();
                        t1.print();
                        if( t1.cmp( t ) ) break;
                    }
                    puts("");
            }
        }
    }
    void solve(){
        memset( vis, 0, sizeof(vis));
        memset( gao, 0, sizeof(gao));
        dep = 0;
        tarjan(1,0);
    }
    int main()
    {
        freopen("1.txt","r",stdin);
        //while( scanf("%d%d", &n,&m) != EOF)
        scanf("%d%d", &n,&m);
        {
            input();
            solve();
        }
        return 0;
    }
    
    /*
    7 9
    1 2
    1 3
    1 6
    1 7
    2 3
    2 4
    2 5
    4 5
    6 7
    
    */
    View Code

    4. 边双连通分量

    poj 3177 Redundant Paths

    题意是使所有的顶点间都有两条路可达, 路径可共顶点但不可共边.

    做法是. 将同一个边双连通分量的所有点收缩成一个点. 新构成的顶点形成一棵树. 

    此时的任意两个顶点间都需要有至少两条相互"独立边", 则可以让成对的叶子节点连边.

    所有总方案数 = (叶子节点数量+1)/2,    单个叶子节点也要连到其它节点去.

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    
    using namespace std;
    
    const int N = 1010;
    
    struct Node{
        int v, nxt;
    }edge[N*100];
    int head[N], idx;
    int n, m;
    int st[N];
    int belong[N], dfn[N], low[N];
    bool vis[N];
    int bridge[N*10][2], nbridge, D[N];
    int find(int x){ return x==st[x]?x:(st[x]=find(st[x]));}
    void Union(int a,int b){
        int x = find(a), y = find(b);
        if( x != y ) st[x] = y;
    }
    void AddEdge(int u,int v)
    {
        edge[idx].v = v;
        edge[idx].nxt = head[u];
        head[u] = idx++;
    }
    void input(){
        memset(head,-1,sizeof(head));
        idx = 0;
        for(int i = 0; i < m; i++)
        {
            int u, v;
            scanf("%d%d", &u,&v); u--, v--;
            AddEdge(u,v), AddEdge(v,u);
        }
    }
    void tarjan(int u,int pre,int dep){
        vis[u] = true;
        dfn[u] = low[u] = dep;
        for(int i = head[u]; ~i; i = edge[i].nxt)
        {
            int v = edge[i].v;
            if( v == pre ) continue;
            if( vis[v] ) low[u] = min( low[u], dfn[v] );
            else{
                tarjan( v,u,dep+1);
                low[u] = min( low[u], low[v] );
                if( dfn[u] >= low[v] ) Union(u,v);
                if( dfn[u] <  low[v] ){
                    bridge[nbridge][0] = u; bridge[nbridge++][1] = v;
                }
            }
        }
    }
    
    int GetConnection(){
        for(int i = 0; i < n; i++) st[i] = i;
        nbridge = 0;
        memset(vis,0,sizeof(vis));
        memset(belong,-1,sizeof(belong));
        tarjan(0,-1,1);
    
        int conn = 0;
        for(int i = 0; i < n; i++)
        {
            int k = find(i);
            if( belong[k] == -1 ) belong[k] = conn++;
            belong[i] = belong[k];
        }
        return conn;
    }
    
    void solve(){
        int w = GetConnection();
        memset( D, 0, sizeof(D));
        for(int i = 0; i < nbridge; i++)
        {
            int u = bridge[i][0], v = bridge[i][1];
            D[ belong[u] ]++, D[ belong[v] ]++;
        }
        int cnt = 0;
        for(int i = 0; i < w; i++)
            if( D[i] == 1 ) cnt++;
        printf("%d
    ", (cnt+1)/2 );
    }
    int main(){
        while( scanf("%d%d", &n,&m) != EOF)
        {
            input();
            solve();
        }
        return 0;
    }
    View Code

    5. 点连通度

    6. 边连通度

    7. 无向图强连通分量缩点建树

    hdu 4612 warm up

    题目算法很好想. 添加一条边使"割边"最小. 则将 所有的强连通分量缩成一个点.然后构成一棵树.之后求出树上最长路,

    这时候将最长路连起来,剩下的桥固然最小.

    无向图强连通分量求法同 有向差不多.  可以选择 询问完所有子节点后利用 dfn[u]==low[u] 压栈合并. 也可以在里头用 low[v] <= dfn[u]

    条件判定后用 并查集合并.

    不管有向图,还是无向图. 要特别注意重边. 求割边主要是否要标记边.  这里有重边.就可以将两个顶点合并成强连通分量.

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<stack>
    #include<algorithm>
    using namespace std;
    #pragma comment(linker, "/STACK:1024000000,1024000000")
    
    const int N = 200010;
    const int M = 1000010;
    
    struct Edge{
        int v, nxt, flag;
    }edge[M<<2];
    int head[N], idx;
    int n, m;
    
    int dfn[N], low[N], dep, vis[N];
    int id[N], conn;
    bool instack[N];
    int nbridge, bridge[M][2];
    int maxlen;
    
    stack<int> st;
    
    void AddEdge(int u,int v){
        edge[idx].v = v; edge[idx].nxt = head[u];
        edge[idx].flag = 1; head[u] = idx++;
        edge[idx].v = u; edge[idx].nxt = head[v];
        edge[idx].flag = 1; head[v] = idx++;
    }
    void input(){
        memset(head,-1,sizeof(head));
        idx = 0;
        for(int i = 0; i < m; i++)
        {
            int u, v;
            scanf("%d%d", &u,&v);
            AddEdge(u,v);
        }
    }
    
    void tarjan(int u,int pre)  // 无向图,数据有回边.需要将其看做不同边.且边需要标记...
    {
        vis[u] = true;
        dfn[u] = low[u] = ++dep;
        st.push(u); instack[u] = true;
        for(int i = head[u]; ~i; i = edge[i].nxt )
        {
            int v = edge[i].v;
            if( edge[i].flag == false ) continue;
            edge[i].flag = edge[i^1].flag = false;
            if( !vis[v] ){
                tarjan(v,u);
                low[u] = min( low[u], low[v] );
                if( (dfn[u] < low[v]) ){
                    bridge[ nbridge ][0] = u;
                    bridge[ nbridge++ ][1] = v;
                }
            }
            else if( instack[v] )
                low[u] = min( low[u], dfn[v] );
        }
        if( dfn[u] == low[u] )
        {
            int t;
            do{
                id[t=st.top()] = conn; st.pop();
                instack[t] = false;
            }while(t!=u);
            conn++;
        }
    }
    void GetConnect(){
        memset(vis,0,sizeof(vis));
        memset(instack,0,sizeof(instack));
        nbridge = 0;
        conn = 0; dep = 0;
        while( !st.empty() ) st.pop();
        for(int i = 1; i <= n; i++)
            if( !vis[i] ) tarjan(i,0);
    
        // Debug
       /* for(int i = 1; i <= n; i++)
            printf("dfn[%d] = %d, low[%d] = %d
    ", i,dfn[i], i,low[i]);
        for(int i = 1; i <= n; i++)
            printf("id[%d] = %d
    ", i, id[i] );*/
    }
    void ReGraph(){
        memset(head,-1,sizeof(head));
        idx = 0;
        for(int i = 0; i < nbridge; i++){
            int u = id[ bridge[i][0] ], v = id[ bridge[i][1] ];
           // printf("u = %d, v = %d
    ", u, v );
            AddEdge(u,v);
        }
    }
    
    int dfs(int u,int pre)
    {
        int tmp = 0;
        for(int i = head[u]; ~i; i = edge[i].nxt )
        {
            int v = edge[i].v;
            if( v == pre ) continue;
    
            int d = dfs(v,u);
            maxlen = max( maxlen, tmp+d );
            tmp = max(tmp,d);
        }
        return tmp+1;
    }
    void solve(){
        GetConnect();
        ReGraph();
    
        maxlen = 0;
        dfs(0,-1);
    
       // printf("maxlen = %d, conn = %d
    ", maxlen, conn);
        printf("%d
    ", conn-(maxlen+1) );
    }
    int main(){
        while( scanf("%d%d", &n,&m) != EOF  )
        {
            if(n + m == 0) break;
            input();
            solve();
        }
        return 0;
    }
    View Code

    二 有向图

    1. 有向图强连通分量

    当 dfn[u] == dfn[v]时,以u为根的搜索子树上所有结点是一个强连通分量.

    Tarjan( u )
    {
        dfn[u] = low[u] = ++tmpdfn;
        Stack.push( u );
        for each (u,v) in E
        { 
            if( v is not visit )   // 此时当前边是生成树的边
            {
                tarjan( v );
                low[u] = min( low[u], low[v] )    
            }
            else if( v is in stack ) // 此时为回边
                low[u] = min( low[u], dfn[v] )
            // else 此时为 交叉边,其他连同分量上的边. 
        }
        if( dfn[u] == low[u] )
            repeat
                v = stack.pop;  将v腿栈,为该连通分量的一个顶点
                print v
            until( u == v )
    }

    poj 2676 Going from u to v or from v to u?

    题目大意是给一个有向图,让判定该图是否任意两点可达.( u -> v or v->u )

    算法是, 先求出所有强连通分量,然后进行缩点.构成新图. 然后对新图做拓扑排序, 若拓排后的序列相邻间都有边相连.则符合要求.

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    #include<algorithm>
    #include<stack>
    #include<queue>
    using namespace std;
    #pragma comment(linker, "/STACK:1024000000,1024000000")
    
    const int N = 1010;
    
    int n, m;
    struct Node{
        int v, nxt;
    }edge[N*100];
    int head[N], idx;
    
    int dfn[N], low[N], id[N], dep;
    bool vis[N], instack[N];
    int graph[N][N];
    int D[N], topo[N];
    int conn;
    
    stack<int> st;
    void AddEdge(int u,int v)
    {
        edge[idx].v = v; edge[idx].nxt = head[u];
        head[u] = idx++;
    }
    void input(){
        memset(head,-1,sizeof(head));
        idx = 0;
        for(int i = 0; i < m; i++)
        {
            int u, v;
            scanf("%d%d", &u,&v);
            AddEdge(u,v);
        }
    }
    
    void tarjan(int u,int pre)
    {
        st.push(u); instack[u] = true;
        vis[u] = true; dfn[u] = low[u] = ++dep;
        for(int i = head[u]; ~i; i = edge[i].nxt )
        {
            int v = edge[i].v;
        //    if( v == pre ) continue;
            if( !vis[v] ){ // 生成树的边.
                tarjan(v,u);
                low[u] = min( low[u], low[v] );
            }
            else if( instack[v] ) // 在栈中.回边.
                low[u] = min( low[u], dfn[v] );
        }
        if( dfn[u] == low[u] ){   //顶点u为根的子树是一个强连同块
            int t;
            do{
                id[ t=st.top() ] = conn; st.pop();
                instack[t] = false; //low[t] = n;
            }while( t != u );
            conn++; //强连通分量增加
        }
    }
    
    int TopSort(){
        memset( D, 0, sizeof(D));
        memset( vis, 0, sizeof(vis));
        while( !st.empty() ) st.pop();
        for(int i = 0; i < conn; i++){
            for(int j = 0; j < conn; j++)
                if(i != j) D[i] += graph[j][i];
            if( D[i] == 0 ) st.push(i), vis[i] = true;
        }
      //  if( st.size() > 1 ) return 0;
        int tot = 0;
        while( !st.empty() )
        {
            int u = st.top(); st.pop(); vis[u] = false;
            topo[tot++] = u;
            //printf("TopSort:  u = %d
    ", u );
            for(int v = 0; v < conn; v++)
            if( graph[u][v] )
            {
                if(u == v) continue;
                D[v] -= graph[u][v];
                if( D[v] == 0 && !vis[v] ) st.push(v), vis[v] = true;
            }
        }
        if( tot < conn ) return 0;
        return 1;
    }
    void solve(){
        conn = 0; dep = 0;
        while( !st.empty() ) st.pop();
        memset(vis,0,sizeof(vis));
        memset(instack,0,sizeof(instack));
    
        for(int i = 1; i <= n; i++)
            if( !vis[i] ) tarjan(i,0);
        memset( graph, 0, sizeof(graph));
        for(int u = 1; u <= n; u++)
        {
            for(int i = head[u]; ~i; i = edge[i].nxt )
            {
                int v = edge[i].v;
                graph[ id[u] ][ id[v] ] = 1;
            }
        }
        if( !TopSort() ) puts("No");
        else
        {
            //for(int i = 0; i < conn; i++)
           //     printf("%d ", topo[i] ); puts("");
            bool flag = true;
            for(int i = 0; i < conn-1; i++)
                if( graph[ topo[i] ][ topo[i+1] ] == 0 )
                    flag = false;
            puts( flag ? "Yes" : "No");
        }
    
    }
    int main(){
        freopen("1.txt","r",stdin);
        int T;
        scanf("%d", &T);
        while( T-- )
        {
            scanf("%d%d", &n,&m);
            input();
            solve();
        }
        return 0;
    }
    View Code

    poj 2186 Popular Cows

    可以推出,若强连通分量中的一头牛A, 仰慕强连通分量外的一头牛B, 则连同分量内的其他牛都会仰慕牛B.

    所以我们可以将 每个强连通分量 收缩成一个顶点. 然后因为要求 受所有牛仰慕的牛的数量. 所以这个顶点.

    要么是一个 强连通缩点,要么就是单独一头牛. 此时应只有当前一个顶点 出度为0. 否则矛盾.

    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    #include<stack>
    #include<algorithm>
    using namespace std;
    
    const int N = 10010;
    
    struct Edge{
        int v, nxt;
    }edge[N*10];
    int head[N], idx;
    int n, m;
    
    int dfn[N], low[N], dep;
    int id[N], conn, num[N];
    bool vis[N], instack[N];
    int D[N];
    stack<int> st;
    
    void AddEdge(int u,int v)
    {
        edge[idx].v = v; edge[idx].nxt = head[u];
        head[u] = idx++;
    }
    void input()
    {
        memset(head,-1,sizeof(head));
        idx = 0;
        for(int i = 0; i < m; i++)
        {
            int u, v;
            scanf("%d%d", &u,&v);
            AddEdge(u,v);
        }
    }
    void tarjan(int u,int pre){
        st.push(u); instack[u] = true;
        vis[u] = true;
        dfn[u] = low[u] = ++dep;
        for(int i = head[u]; ~i; i = edge[i].nxt )
        {
            int v = edge[i].v;
    
            if( !vis[v] ){
                tarjan(v,u);
                low[u] = min( low[u], low[v] );
            }
            else if( instack[v] )
                low[u] = min( low[u], dfn[v] );
        }
        if( dfn[u] == low[u] )
        {
            int t;
            do{
                id[ t=st.top() ] = conn; st.pop();
                instack[t] = false;
                num[ conn ]++;   // 该连通分量节点数量
            }while( t != u );
            conn++;
        }
    }
    int GetConnect(){
        conn = 0; dep = 0;
        memset(vis,0,sizeof(vis));
        memset(instack,0,sizeof(instack));
        for(int i = 1; i <= n; i++)
            if( !vis[i] ) tarjan(i,0);
    
       /* for(int i = 1; i <= n; i++){
            printf("dfn[%d] = %d, low[%d] = %d
    ", i, dfn[i], i, low[i] );
            printf("id[%d] = %d
    ", i, id[i] );
        }*/
        return conn;
    }
    void solve()
    {
        GetConnect();
    
        for(int u = 1; u <= n; u++)
        {
            for(int i = head[u]; ~i; i = edge[i].nxt )
            {
                int v = edge[i].v;
                if( id[u] != id[v] ) D[ id[u] ]++;
            }
        }
        int ts = 0, rs = 0;
        for(int i = 0; i < conn; i++)
        {
            if( D[i] == 0 ) ts++, rs = i;
        }
        if( ts == 1 ) printf("%d
    ", num[rs] );
        else puts("0");
    
    }
    int main()
    {
        //freopen("1.txt","r",stdin);
        while( scanf("%d%d", &n,&m) != EOF )
        {
            input();
            solve();
        }
        return 0;
    }
    View Code
  • 相关阅读:
    数据结构:关于重建二叉树的三种思路
    操作系统:进程调度算法详解之FCFS和SPF篇
    Java反射机制浅析
    数据挖掘:基于TF-IDF算法的数据集选取优化
    RC隔离 更新where条件列 没有索引的情况
    RR区间锁 不是唯一索引,即使区间内没值,也锁
    If one session has a shared or exclusive lock on record R in an index, another session cannot insert
    RR模式下利用区间锁防止幻读,RC模式没有区间锁会出现幻读
    使用next-key locks 用于搜索和索引扫描,可以防止幻读
    Gap Locks 区间锁
  • 原文地址:https://www.cnblogs.com/yefeng1627/p/3216952.html
Copyright © 2011-2022 走看看