• 【刷题】【记录】牛客题单


    题单地址:

    【237题】算法基础精选题单_ACM竞赛_ACM/CSP/ICPC/CCPC/比赛经验/题解/资讯_牛客竞赛OJ_牛客网 (nowcoder.com)

    【算法进阶题单】动态规划、数据结构、图论、数学_ACM竞赛_ACM/CSP/ICPC/CCPC/比赛经验/题解/资讯_牛客竞赛OJ_牛客网 (nowcoder.com)

    第一章 模拟、枚举和贪心

    模拟

    NC16644 [NOIP2007]字符串的展开

    #include<iostream>
    #include<algorithm>
    using namespace std;
    
    //isalpha() 函数 
    //默认是大小写英文字母,范围可改
    
    //--情况,wa10%
    bool isnumber(char ch)
    {
        if(ch>='0' && ch<='9')
            return true;
        else return false;
    }
    
    string get_char(char l,char r,int p1,int p2,int p3)
    {
        //减号两侧同为小写字母或同为数字,且按照ASCII码的顺序,减号右边的字符严格大于左边的字符。
        if( ( isalpha(l) && isalpha(r) && l<r ) || ( isnumber(l) && isnumber(r) && l<r ) )
        {
            string ans="";
            for(char t=l+1;t<r;t++)
            {
                for(int j= 0;j<p2;j++)
                    ans+=t;
            }
            int ans_len=ans.length();
            if( p1==2 && isalpha(l) )
            {
                for(int i=0;i<ans_len;i++)
                    ans[i] = ans[i]-'a'+'A';
            }
            if(p1==3)
            {
                for(int i=0;i<ans_len;i++)
                    ans[i] = '*';
            }
            if(p3==2)
                reverse(ans.begin(),ans.end());
            return ans;
        }
        else return "-";
    }
    
    int main()
    {
        int p1,p2,p3;
        cin >> p1 >> p2 >> p3;
        string s;
        cin >> s;
    
        string ans = "";
    
        int s_len = s.length();
        for(int i=0;i<s_len;i++)
        {
            //边界判断,wa10%
            if(s[i]=='-' && i+1<s_len && i-1>=0 )
                ans += get_char(s[i-1],s[i+1],p1,p2,p3);
            else
                ans += s[i];
        }
    
        cout << ans;
        return 0;
    }
    View Code

    枚举

    思路:合适的枚举对象、顺序、方法

    合适的枚举顺序:

    NC16593 [NOIP2011]铺地毯【逆向思维】

    //思路,单点查询 => 倒序寻找
    
    #include <bits/stdc++.h>
    using namespace std;
    
    const int N=1e4+10;
    int memory[N][4];
    
    int main()
    {
        int n;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            for(int j=0;j<4;j++)
                cin>>memory[i][j];
        }
    
        int x,y;
        cin>>x>>y;
    
        for(int i=n;i>=1;i--)
        {
            //铺设地毯的左下角的坐标(a,b)以及地毯在x轴和y轴方向的长度。
            if(memory[i][0] <= x && memory[i][1] <= y )
                if(memory[i][0]+memory[i][2]-1 >= x && memory[i][1]+memory[i][3]-1 >= y )
                {
                    printf("%d\n",i);
                    return 0;
                }
        }
    
        printf("-1\n");
        return 0;
    }
    View Code

    第二章: 递归、分治

    递归:

    NC15173 The Biggest Water Problem

    #include<bits/stdc++.h>
    using namespace std;
    
    long long get_sum(long long x)
    {
        long long t=0;
        while(x)
        {
            t+=x%10;
            x/=10;
        }
        return t;
    }
    
    int main()
    {
        long long a;
        long long sum=0;
        cin>>a;
        while(a>0)
        {
            sum = get_sum(a);
            if(sum < 10 )
            {
                printf("%lld",sum);
                return 0;
            }
            a=sum;
        }
        return 0;
    }
    View Code

    分治:

    NC16660 [NOIP2004]FBI树

    // 我们可以把由“0”和“1”组成的字符串分为三类:全“0”串称为B串,全“1”串称为I串,既含“0”又含“1”的串则称为F串。
    // FBI树是一种二叉树[1],它的结点类型也包括F结点,B结点和I结点三种。由一个长度为2N的“01”串S可以构造出一棵FBI树T,递归的构造方法如下:
    // 1) 根结点为R,其类型与串S的类型相同;
    // 2) 若串S的长度大于1,将串S从中间分开,分为等长的左右子串S1和S2;由左子串S1构造R的左子树T1,由右子串S2构造R的右子树T2。
    // 现在给定一个长度为2N的“01”串,请用上述构造方法构造出一棵FBI树,并输出它的后序遍历[2]序列。
    
    #include <bits/stdc++.h>
    using namespace std;
    
    const int N=1e5+10;//数组大小猜的
    int rt;
    struct node
    {
        char v;
        int lc,rc;
    }tree[N];
    int tot;
    
    string s;
    
    char find(int l,int r)
    {
        bool find_0=false,find_1=false;
        for(int i=l;i<=r;i++)
        {
            if(find_0 && find_1 ) 
                break;
            if(s[i] == '0')
                find_0=true;
            if(s[i] == '1')
                find_1=true;
        }
        
        if(find_0 && find_1 )
            return 'F';
        else if(find_0 )
            return 'B';
        else
            return 'I';
    }
    
    void build(int &root,int l,int r)
    {
        root=tot++;
        tree[root].lc = tree[root].rc = -1;
        tree[root].v = find(l,r);
    
        if(l == r)
            return ;
        else
        {
            int mid = (l+r)>>1;
            build(tree[root].lc,l,mid);
            build(tree[root].rc,mid+1,r);
            return ;
        }
    }
    
    void post_order(int root)
    {
        if(tree[root].lc !=-1)
            post_order(tree[root].lc);
        if(tree[root].rc !=-1)
            post_order(tree[root].rc);
        printf("%c",tree[root].v);
    }
    
    int main()
    {
        int n; cin>>n;
        cin>>s;
    
        build(rt,0,s.length()-1);
        post_order(rt);
    
        return 0;
    }
    View Code

    第三章: 二分、三分、01分数规划

    二分:

    二分查找:

    NC235558 牛可乐和魔法封印

    #include<bits/stdc++.h>
    using namespace std;
    
    int n,q;
    const int N=1e5+10;
    int d[N];
    
    int main()
    {
        cin>>n;
        for(int i=0;i<n;i++)
            cin>>d[i];
        sort(d,d+n);
    
        cin>>q;
        while(q--)
        {
            int a,b;
            cin>>a>>b;
            //似乎不能使用L,R作为变量名,会出错
            int lpos= lower_bound(d,d+n,a)-d ;
            int rpos= upper_bound(d,d+n,b)-d ; 
            printf("%d\n",rpos-lpos);
        }
    
        return 0;
    }
    View Code

    第四章: 栈、队列

    栈:

    NC14326 Rails

    #include<bits/stdc++.h>
    using namespace std;
    
    int n,q;
    const int N=1e3+10;
    int d[N];
    
    stack <int > sta;
    
    int main()
    {
        int n;
        bool first = true;
        while(scanf("%d",&n) && n!=0 )
        {
            if(!first )
                printf("\n");
            while(scanf("%d",&d[0]) && d[0]!=0 )
            {
                if(!first )
                    printf("\n");
                else first=false;
    
                while(!sta.empty() ) sta.pop();
                for(int i=1;i<n;i++)
                    cin>>d[i];
                
                int nw=0,pos=1;
                bool fail=false;
                while(nw<n && !fail )
                {
                    if(!sta.empty() && d[nw] == sta.top() )
                        sta.pop(),nw++;
                    else if(d[nw] < pos )
                        fail=true;
                    else if(d[nw] == pos )
                        nw++,pos++;
                    else 
                        sta.push(pos++);
                }
                if(!fail )
                    printf("Yes");
                else printf("No");
            }
    
        }
    
        return 0;
    }
    View Code

    队列:

    NC13822 Keep In Line

    #include <bits/stdc++.h>
    using namespace std;
    
    int T,n;
    queue <int > q;
    
    int tot;
    map <string ,int > id; 
    
    const int N=1e5+10;
    bool out[N];
    
    int main()
    {
        int cnt=0;//不插队人数
    
        cin>>T;
        while(T--)
        {
            while(!q.empty() )
                q.pop();
            tot = 0;
            id.clear();
            cnt = 0; //这个不写wa一次...
    
            string s1,s2;
            cin>>n;
            for(int i=1;i<=n;i++)
            {
                cin>>s1>>s2;
                if(s1[0]=='i' )
                {
                    id[s2] = ++tot;
                    q.push(tot);
                    cnt++;
                    //cout << s2 << " "<<tot<<endl;
                    //cout<<cnt<<endl;
                }
                else
                {
                    if(q.front() != id[s2] )
                    {
                        cnt--;
                        out[id[s2]]=true;
                    }
                    else
                    {
                        q.pop();
                        while(!q.empty() && out[q.front()] )
                            q.pop();
                    }
                    //cout<<cnt<<endl;
                }
            }
            printf("%d\n",cnt);
            
            for(int i=1;i<=tot;i++)
                out[i]=false;
        }
    
        return 0;
    }
    View Code

    第五章:优先队列、并查集

    堆:

    模板:

    NC16663 合并果子

    #include<bits/stdc++.h>
    using namespace std;
    
    int main()
    {
        priority_queue <int , vector <int > ,greater <int > > q;
    
        int n;
        cin >> n;
        for(int i=1,x;i<=n;i++)
            cin>>x,q.push(x);
    
        long long sum = 0;
        while(!q.empty() )
        {
            long long t = q.top(); q.pop();
            if(q.empty() ) break;
            
            t += q.top();
            q.pop();
            q.push(t);
            sum += t ;
        }
        
        cout<<sum;
        return 0;
    }
    View Code

    NC214362 第k小

    #include <bits/stdc++.h>
    using namespace std;
    
    int n,m,k;
    const int N=2e5+10;
    //greater 改成小根堆
    priority_queue <int ,vector <int > ,greater <int > > q1; //放置 k+1 到 n 的数字
    priority_queue <int ,vector <int > ,less <int > > q2; //放置 1 到 k 的数字
    
    void get(int x) //限制条件为,q2一定满了,q1中可能有数
    {
        int cnt_sub = 0 ;
        if(q2.top() > x) //实际上x只会挤走一个q2中的数
        {
            q1.push(q2.top() );
            q2.pop();
            q2.push(x);
        }
        else
            q1.push(x);
    }
    
    int main()
    {
        cin>>n>>m>>k;
        int nw_n=0;
    
        int v;
        for(int i=1;i<=n;i++)
        {
            cin>>v;
            nw_n ++;
            if(nw_n <= k ) q2.push(v);
            else get(v);
        }
    
        int op;
        while(m--)
        {
            cin>>op;
            if(op==1 )
            {
                cin>>v;
                nw_n++;
                if(nw_n <= k ) q2.push(v);
                else get(v);
            }
            else
            {
                if(nw_n < k )
                    printf("-1\n");
                else 
                    printf("%d\n",q2.top() );
            }
        }
    
        return 0;
    }
    View Code

    第六章:搜索

    DFS:

    回溯思想:

    NC15128 老子的全排列呢

    #include<bits/stdc++.h>
    using namespace std;
    
    bool us[10];
    int d[10];
    
    void dfs(int pos)
    {
        if(pos >= 8 ) 
        {
            for(int i=0;i<8;i++)
            {
                if(i!=0)
                    printf(" ");
                printf("%d",d[i]);
            }
            printf("\n");
            return ;
        }
        else
        {
            for(int i=1;i<=8;i++)
                if(!us[i])
                {
                    d[pos] = i;
                    us[i] = true;
                    dfs(pos+1);
                    us[i] = false;
                }
        }
    }
    
    int main()
    {
        dfs(0);
    
        return 0;
    }
    View Code

    BFS:

    NC15136 迷宫

    #include <bits/stdc++.h>
    using namespace std;
    
    int n,m;
    const int N=510;
    
    struct node
    {
        int x,y;
        bool key;
        int step;
    };
    queue <node > q;
    
    int py[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
    
    int mp[N][N];
    int step[N][N][2];
    //0表示能走,1表示钥匙,-1表示墙壁,D表示门锁
    
    int main()
    {
        int sx,sy,ex,ey;
    
        cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
            {
                char ch = getchar();
                while(ch!='W' && ch!='K' && ch!='S' && ch!='D' && ch!='E' && ch!='.')
                    ch = getchar();
                if(ch == 'W' )
                    mp[i][j]=-1;
                else if(ch == 'K' )
                    mp[i][j]=1;
                else if(ch == 'D' )
                    mp[i][j]=2;
                else if(ch == 'S')
                    sx = i , sy = j;
                else if(ch == 'E' )
                    ex = i , ey = j;
            }
        }
    
    //     for(int i=1;i<=n;i++)
    //     {
    //         for(int j=1;j<=m;j++)
    //         {
    //             printf("%3d ",mp[i][j]);
    //         }
    //         printf("\n");
    //     }
        //printf("%d %d \n",ex,ey);
        q.push({sx,sy,false,0});
        while(!q.empty() )
        {
            node t = q.front();
            q.pop();
    
            for(int i=0;i<4;i++)
            {
                int nx = t.x + py[i][0];
                int ny = t.y + py[i][1];
    
                if(nx<1 || ny<1 || nx>n || ny>m )
                    continue;
                if(mp[nx][ny] == -1 )
                    continue;
                if(mp[nx][ny] == 2 && !t.key )
                    continue;
                
                bool t_key = t.key;
                if(mp[nx][ny] == 1 ) t_key = true;
                
                if(step[nx][ny][t_key]>0 ) continue;
                step[nx][ny][t_key] = t.step +1;
                //printf("%d %d %d %d\n",nx,ny,t_key,t.step+1);
                
                if(nx == ex && ny == ey )
                {
                    printf("%d\n",t.step+1);
                    return 0;
                }
    
                q.push({nx,ny,t_key,t.step+1});    
            }
        }
        printf("-1\n");
        return 0;
    }
    View Code

    第七章:

    递推:

    NC235911 走楼梯

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=110;
    int f[N];
    
    int main()
    {
        int n;
        cin>>n;
    
        f[0]=1,f[1]=1;
        for(int i=2;i<=n;i++)
            f[i] = f[i-1]+f[i-2];
        printf("%d\n",f[n]);
        return 0;
    }
    View Code

    线性dp:

    NC22096 数字三角形(什么算法也没用,很水)

    #include <bits/stdc++.h>
    using namespace std;
    
    int main()
    {
        int n;
        while(~scanf("%d",&n) )
        {
            for(int i=1;i<=n;i++)
            {
                for(int j=1;j<=i;j++)
                {
                    if(j!=1 )
                        printf(" ");
                    printf("%d",j);
                } 
                printf("\n"); 
            }
        }
        return 0;
    }
    View Code

    背包问题:

    01背包:

    NC16693 装箱问题

    #include <bits/stdc++.h>
    using namespace std;
    
    const int M=2e5+10;
    bool f[M];
    
    int main()
    {
        int V,n;
        cin>>V>>n;
        
        f[0]=true;
        for(int i=1;i<=n;i++)
        {
            int v; cin>>v;
            for(int j=V;j>=v;j--)
            {
                if(f[j-v] )
                    f[j]=true;
            }
        }
        for(int j=V;j>=0;j--)
            if(f[j] )
            {
                printf("%d",V-j);
                return 0;
            }
        return 0;
    }
    View Code

    第八章:

    树形dp:

    【基本概念-笔记】:【笔记】树形dp - 心若笺诗 - 博客园 (cnblogs.com)

    【基本算法-笔记】:【笔记】【树形dp】 - 心若笺诗 - 博客园 (cnblogs.com)

    NC15033 小G有一个大树

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1e3+10;
    int n;
    vector <int > son[N];
    int point,num;
    
    int f[N],sum[N];
    //sum是x点子树的所有节点数,不包括自己
    //f是去掉x,x点子树中节点最多的连通块
    
    void dfs(int x,int fr)
    {
        int sz=son[x].size();
        sum[x] = 1;
    
        for(int i=0;i<sz;i++)
        {
            int nx=son[x][i];
            if(nx==fr ) continue;
    
            dfs(nx,x);
            sum[x] += sum[nx];
        }
        for(int i=0;i<sz;i++)
        {
            int nx=son[x][i];
            if(nx==fr ) continue;
    
            f[x] = max(f[x],sum[nx]);
        }
    
        int t = max(f[x],n-sum[x]);
        if(t < num )
            num=t,point=x;
    }
    
    int main()
    {
        cin>>n;
        for(int i=1;i<n;i++)
        {
            int u,v;
            cin>>u>>v;
            son[u].push_back(v);
            son[v].push_back(u);
        }
        
        num=n;
        dfs(1,0);
    
        printf("%d %d",point,num);
    
        return 0;
    }
    View Code

    第九章:

    基本概念:

    NC16679 [NOIP2003]神经网络

    (本题简略题解:【图论】【刷题】【蓝绿题】【强连通】【拓扑】 - 心若笺诗 - 博客园 (cnblogs.com)

    #include<cstdio>
    #include<cstdlib>
    #include<queue>
    #include<vector>
    using namespace std;
    int n,m;
    const int N=103;
    int c[N],in[N],sz[N];
    struct node
    {
        int v,w;
        node(int vv,int ww)
        { v=vv,w=ww; }
        node(){}
    };
    vector <node > g[N];
    queue <int > q;
    
    void topo_sort()
    {
        while(!q.empty())
        {
            int t=q.front();q.pop();
            sz[t]=g[t].size() ;
            
            for(int i=0;i<sz[t];i++)
            {
                node v=g[t][i];
                if(c[t]>0) c[v.v ]+=c[t]*v.w ;
                if(--in[v.v ]==0) q.push(v.v );
            }
        }
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        int x;
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&c[i],&x);
            if(c[i]>0) q.push(i);
            else c[i]-=x;
        }
        int u,v,w;
        while(m--)
        {
            scanf("%d%d%d",&u,&v,&w);
            g[u].push_back(node(v,w)); 
            in[v]++;
        }
        
        topo_sort();
        bool fail=true;
        for(int i=1;i<=n;i++)
            if(!sz[i] && c[i]>0)
            {
                printf("%d %d\n",i,c[i]);
                fail=false;
            }
        if(fail) printf("NULL\n");
        return 0;
    }
    View Code

    NC20115 [HNOI2015]菜肴制作

    (感觉还蛮有写头的,所以另开了篇题解)

    第十章:

    数论:

    NC14399 素数判断

    #include<bits/stdc++.h>
    using namespace std;
    
    int T;
    
    vector <int > v;
    const int N=1e5;
    bool notprime[N+10];
    void prepare()
    {
        for(int i=2;i<=N;i++)
            if(!notprime[i] )
            {
                v.push_back(i);
                for(int j=2;i*j<=N;j++)
                    notprime[i*j]=true;
            }
    }
    
    int sz;
    
    bool check (int x)
    {
        for(int i=0;i<sz && v[i]<x ;i++)
            if(x%v[i] == 0 ) return false;
        return true;
    }
    
    int main()
    {
        prepare(); //1e5范围内的素数都取出来
        sz = v.size();
    
        cin>>T;
        while(T--)
        {
            int n;
            cin>>n;
            
            if(check(n) )
            {
                printf("isprime\n");
                printf("%d\n",n);
            }
            else
            {
                printf("noprime\n");
                bool first = true;
                for(int i=0; i<sz && v[i]<=n ;i++)
                {
                    if(n%v[i] == 0 )
                    {
                        if(first )
                            first = false;
                        else printf(" ");
                        
                        printf("%d",v[i]);
                        while(n % v[i] == 0 ) 
                            n/=v[i];
                    }
                }
                //因为下面这句话我wa了4个点...
                if(n != 1 )
                    printf(" %d",n);
                //比如p>1e5且p是素数,原来算法对于2p,不会输出p
                printf("\n");
            }
        }
        
        return 0;
    }
    View Code

    第十一章:

    线段树:

    NC15164 Big Water Problem

    【代码1】树状数组模板

    #include <bits/stdc++.h>
    using namespace std;
    
    int n;
    const int N = 1e5 + 10;
    int a[N], c[N];
    // a[i]为原始数组
    // c[i]为树状数组
    
    int lowbit(int x)
    {
        return x & -x;
    }
    
    void build() //利用a[i]数组数据建立c[i]
    {
        for (int i = 1; i <= n; i++)
        {
            c[i] = a[i];
            int k = i - lowbit(i) + 1;
            for (int j = k; j < i; j++)
                c[i] += a[j];
        }
    }
    
    int sum(int x)
    {
        int sum = 0;
        while (x > 0)
        {
            sum += c[x];
            x -= lowbit(x);
        }
        return sum;
    }
    
    int query_sum(int l, int r)
    {
        return sum(r) - sum(l - 1);
    }
    
    void add(int pos, int x)
    {
        while (pos <= n)
        {
            c[pos] += x;
            pos += lowbit(pos);
        }
    }
    
    int main()
    {
        int m;
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            cin >> a[i];
    
        build();
    
        int op, a, b;
        while (m--)
        {
            cin >> op >> a >> b;
            if (op == 1)
                add(a, b);
            else
                cout << query_sum(a, b)<<endl;
        }
    
        return 0;
    }
    View Code

    【代码2】线段树模板

    #include <bits/stdc++.h>
    using namespace std;
    
    int n;
    const int N = 1e5 + 10;
    int d[N];
    int rt, tot;
    
    struct node
    {
        int l, r;   //区间[l,r]
        int lc, rc; //左右子节点
        int v;      //区间整体的值
    } tree[N * 3];
    
    void push_up(int x)
    {
        tree[x].v = 0;
        if (tree[x].lc != -1)
            tree[x].v += tree[tree[x].lc].v;
        if (tree[x].rc != -1)
            tree[x].v += tree[tree[x].rc].v;
        return;
    }
    
    void create(int &root, int l, int r)
    {
        if (l > r)
            return;
        root = tot++;
        tree[root].l = l, tree[root].r = r;
    
        if (l == r)
        {
            tree[root].lc = tree[root].rc = -1;
            tree[root].v = d[l];
        }
        else
        {
            int mid = (l + r) >> 1;
            create(tree[root].lc, l, mid);
            create(tree[root].rc, mid + 1, r);
            push_up(root);
        }
    }
    
    void change(int root, int pos, int v) //单点修改
    {
        if (tree[root].l == tree[root].r)
            tree[root].v = v;
        else
        {
            int mid = (tree[root].l + tree[root].r) >> 1;
            if (pos <= mid)
                change(tree[root].lc, pos, v);
            else
                change(tree[root].rc, pos, v);
            push_up(root);
        }
    }
    
    int query(int root, int l, int r) //当前查找的节点为root,l,r为需要查找的区间看两者是否符合
    {
        if (l <= tree[root].l && tree[root].r <= r)
            return tree[root].v;
        else
        {
            int mid = (tree[root].l + tree[root].r) >> 1, sum = 0;
            if (l <= mid)
                sum += query(tree[root].lc, l, r);
            if (r > mid)
                sum += query(tree[root].rc, l, r);
            return sum;
        }
    }
    
    int main()
    {
        int m;
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            cin >> d[i];
    
        create(rt, 1, n);
    
        while (m--)
        {
            int op, a, b;
            cin >> op >> a >> b;
            if (op == 1)
            {
                d[a]+=b;
                change(rt, a, d[a]);
            }
            else
                cout << query(rt, a, b) << endl;
        }
    
        return 0;
    }
    View Code

    第十二章:

    LCA模板题

    DFS序:

    NC13611 树(dfs序+区间dp)

    做不倒,现学数论在补了已经呜呜

     

  • 相关阅读:
    Hbase记录-Hbase shell使用
    Hbase记录-Hbase基础概念
    JAVA记录-SpringMVC集成redis
    JAVA记录-redis缓存机制介绍(四)
    JAVA记录-redis缓存机制介绍(三)
    JAVA记录-redis缓存机制介绍(二)
    JAVA记录-redis缓存机制介绍(一)
    JAVA记录-SpringMVC scope属性的两种模式
    JAVA记录-JDBC介绍
    鼠标拖动,改变列表宽度
  • 原文地址:https://www.cnblogs.com/xwww666666/p/16466959.html
Copyright © 2020-2023  润新知