分类: 近期发布

NFのBlog 近期发布的一些文章,欢迎观看~

  • 「编程笔记」ST表的实现与使用

    ST表,全称SparseTable,也叫做稀疏表。是用来解决区间和和区间最大最小值问题的一种工具。其实推广开来,其可以用来解决任何可重复贡献(associative)问题。

    使用条件

    可重复贡献问题

    ST表要求其处理的问题必须具有“可重复贡献”这一特征。具体是什么意思呢?

    假设有操作F,使得F(a, b, c) == F(F(a, b), c) == F(a, F(b, c)),则称F符合可重复贡献问题的特征。

    如图,不难发现max()最大值函数就符合可重复贡献问题的特征;读者可以自行验证,sum()min()等函数都符合这个特征,这也证明了ST表可以解决最大最小值以及区间和等问题。

    元数据可不能改变噢

    在使用ST表解决问题时,我们必须确保所有的查询结束前,ST表所对应的元数据都不改变。

    有些数据结构(比如线段树)在元数据更新之后(比如原来的数组中,某个数值由3变成了5),可以有对应的事件复杂度比较低的方法对结构进行更新,以达到一边更新一边查询的效果。ST表则无法提供类似的方法,所以确保你的问题中没有要求在查询期间动态调整元数据的要求。

    SparseTable意义与使用

    接下来,我们采用由表及里的过程,先了解ST表的意义以及使用方法,之后再来说明如何生成ST表。

    一些准备——向下取整的Log2函数

    由于SparseTable的特性,无论是根据数据生成ST表,还是得到ST表后对数据进行查询,都会比较频繁的用到向下取整的log2函数,所以特地说明。我们对于向下取整的log2运算稍加分析,可以得到log2(x)向下取整的具体意义是:

    对于某一个数N,返回可能的最大的数字n,使得2的n次方小于等于N,且2的n+1次方必定大于N。

    比如对于7,log2(7)向下取整为2,2的2次方不超过7,但可以保证2的3次方一定大于7(8>7)。

    了解了这个之后,让我们来看看SparseTable如何快速解决规模庞大的可重复贡献问题吧~

    ST表中数据的意义

    我们接下来选择一个经典的可重复贡献问题——区间最大值问题,作为示例。

    假设我们有一个数组 {3, 2, 1, 4, 5, 9, 7}, 其数据元素个数为7,要求必须在非常低的事件复杂度内,查询某个特定区间(比如从下标0到下标5)的最大值。此时,如果直接循环求解,会导致复杂度随着查询区间长度不断增高,这就是ST表登场的时候啦。

    如上图,对数组进行处理之后,就可以得到下方的二维数组,也就是我们大名鼎鼎的SparseTable——ST表。那么问题来了,图中ST表中的数据,到底具有什么含义呢?

    首先需要知道的是,ST表正常情况下使用一个二维数组st[idx][pow]来储存。下面对这个二维数组每一个位置元素的意义做出解释:

    • 数组中,每一个特定位置(idx, pow)的值,都代表对应问题在某个区间的解。
    • 这个区间的开始坐标为idx。
    • 这个区间的长度为2的pow次方。

    例如,对于区间最大值问题,假设原数组为arr,处理后得到的ST表中,st[1][2] = 5,则说明:原数组,从下标为1开始,长度为2^2=4的区间内,最大值是5。

    特定区间的查询

    知道了ST表的数据的意义,我们就可以着手解决特定区间查询问题。

    这里需要注意的是,虽然ST表可以解决不同的可重复贡献问题,但是其在面对不同的可重复贡献问题时,最优查询方式可能有一定的区别。接下来,我们先通过上方的“区间最大值”例子,介绍一下区间最值问题的ST表查询。

    合适的长度

    首先,为了降低时间复杂度,我们希望用尽可能少的区间查询来拼凑出完整答案。可以证明,在ST表处理区间最值问题时,无论区间长度位置,我们都可以用ST表支持的两个区间,拼接出答案。比如对于区间(1, 6),可以由区间(1, 4)和区间(3, 6)区间的结果再求最值得到。对于“最值”问题,我们只需要保证选择的区间完整覆盖住所求区间,且不超过所求区间即可,而这多个区间允许有重复部分。

    同时,为了保证所用区间尽可能少,每个区间就要尽可能大。这个时候,上方说到的log2向下取整计算就派上用场了。由于ST表代表的区间长度只能是2的n次方倍,所以,不超过区间范围的区间,其最长长度就是log(区间长度)向下取整。

    比如对于(1,6)区间进行最值计算,log2(6)向下取整=2。说明只需要在合适的位置,用两个长度为2^2=4的区间覆盖即可。这代表了,我们要取的数据肯定在ST[idx][2]内——我们确定了pow。

    合适的位置

    我们已经知道了,只要位置安排合理,我们肯定能用两个长度为4的区间覆盖住(1, 6),那么这两个区间的位置,应该如何选择呢?

    思考之后发现,我们肯定希望:

    • 第一个区间尽可能往左,也就是第一个区间的起始下标就是总区间起始下标1。
    • 第二个区间尽可能往右,也就是第二个区间的结束下标就是总区间结束下标6。

    如上图,最终,我们可以得到:

    • 第一个区间为(1, 4),最大值为5。
    • 第二个区间为(3, 6), 最大值为9。

    我们对这两个区间的最大值再求一次最大值即可,最终答案为max(5, 9) = 9

    如果用ST表来计算,运算过程如下:

    max(st[1][2], st[3][2]) 
    = max(5, 9) 
    = 9

    是时候该自己实现ST表啦

    如果您已经掌握了上面所说的,ST表的工作方式,那么恭喜你,欢迎来到ST表的DIY小课堂(bushi),在这里,你将学习如何通过自己的双手书写代码,为一个个数组生成属于他们自己的SparseTable!

    初始化

    初始值

    根据ST表的定义,我们不难得到一个结论,即:

    ST表的第一列数据 st[idx][0],与元数据arr[idx]完全相同。

    更加准确的说,应该是 st[idx][0] == F(arr[idx]),其中F是本ST表所对应解决的可重复贡献问题。而由于我们以区间最大值为例,所以F(arr[idx]) == arr[idx],最终得到st[idx][0] == arr[idx]

    表的长和宽

    解决完初始值,还有另外一个问题,ST表的大小。

    我们已经可以确定,表的行数(第一维下标)就是数据个数,比如对于7个数据的数组,生成的ST表肯定有7行。那么一个ST表有多少列呢?

    ST表有多少列,取决于查询时最多可能用到第几列的数据。根据ST表的区间查询原理,不难得到,对于数据个数为N的数组,其ST表的最大列数,就是log2(N)向下取整。为什么呢?——因为最坏情况下,需要查询整个区间范围的数据,这时,查询区间长度最大,为N,查询时会调用到的区间的纵坐标(上文的pow),正是log2(N)向下取整。

    解决完上面的问题,总算是万事俱备了,我们开始对ST表进行初始化:

    逐步推出整个ST表

    接下来的问题便是,如何计算ST表剩余其他部分的值。我们仍然需要从ST表的定义入手,不难发现,由于ST表中的数据本质上就是某个区间的结果,所以更大区间的结果可以由多个小区间结果拼接而来。意思是,我们可以通过表中左边列数据,一步步推出右边的列。(因为左边的列代表范围更小的区间,右边的列代表范围更大的区间,且相邻列的长度差一定是2倍)

    由上面的思想启发,我们发现,对于某个位置的ST表st[idx][pow],其左边的ST表列数据已经可用(毕竟我们决定从左边往右边推,那么推到pow列的时候,ST表的pow-1列肯定已经完成计算了),其刚好可以由左边列的两个区间st[idx][pow-1]st[idx+2^(pow-1)][pow-1]相结合获得。

    不要看公式复杂就放弃理解噢~,不难发现,其实其思想与上方”ST表的使用“中所提到的”区间查询“,运用了一样的思想。两个区间都尽可能大,同时一个尽量往左,另一个尽量往右。

    上图就是对某个ST表位置进行求值的示意。知道了某个点的求值方法,我们只需要从上到下,从左往右不停循环,直到求出整个ST表即可。

    需要注意,ST表中部分位置没有值,这是因为其对应的区间已经超出范围,比如st[4][2],计算可得,该区间结尾下标为7,但是原始数据个数为7,说明最大下标为6,故该点不应该存在数据,即使存在,查询时也肯定用不到(读者可以自行思考一下,为什么查询的时候不可能调用到这类数据点)。

    实战环节!

    至此,有关于ST表的所有理论思路都已经介绍完成啦,下面我们借用洛谷上的ST表模板题,来介绍一下ST表的实战,以及一些注意事项。

    先放上题目AC代码:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    using LL = int;
    
    using namespace std;
    
    LL st[100000][18] = {0};
    
    LL log2Table[20] = {1};
    
    inline int read()
    {
        int x = 0, f = 1;
        char ch = getchar();
        while (ch < '0' || ch > '9')
        {
            if (ch == '-')
                f = -1;
            ch = getchar();
        }
        while (ch >= '0' && ch <= '9')
        {
            x = x * 10 + ch - 48;
            ch = getchar();
        }
        return x * f;
    }
    
    void initLog2Table()
    {
        for (int i = 1; i < 20; ++i)
        {
            log2Table[i] = log2Table[i - 1] * 2;
        }
        return;
    }
    
    LL logLowerBoundTable[100010] = {0};
    
    // 这里实际上是在对上文提到的那个,重要的log2向下取整函数,进行事先缓存,以提高一些查询效率
    void initLogLowerBound(const LL &numCnt)
    {
        logLowerBoundTable[0] = -1;
        logLowerBoundTable[1] = 0;
        for (LL i = 2; i <= numCnt; ++i)
        {
            logLowerBoundTable[i] = logLowerBoundTable[i >> 1] + 1;
        }
    }
    
    int main()
    {
    
        // init log2 table
        initLog2Table();
    
        // input data
        LL numCnt = 0;
        LL queryCnt = 0;
        numCnt = read();
        queryCnt = read();
        initLogLowerBound(numCnt);
    
        for (LL i = 0; i < numCnt; ++i)
        {
            st[i][0] = read();
        }
        
        // 这里是在计算,对于数据量N,ST表最多需要的列数是多少
        // 上文已经提及
        LL maxLayer = logLowerBoundTable[numCnt];
        for (LL curLayer = 1; curLayer <= maxLayer; ++curLayer)
        {
            // LL rightBound = numCnt - log2Table[curLayer];
            for (LL curIndex = 0; curIndex <= numCnt - log2Table[curLayer]; ++curIndex)
            {
                // calculate current arr value
                st[curIndex][curLayer] = max(st[curIndex][curLayer - 1], st[curIndex + log2Table[curLayer - 1]][curLayer - 1]);
                // cout << st[curIndex][curLayer] << "   ";
            }
            // cout << endl;
        }
    
        // deal with query
        LL left, right, intervalSize, maxLogNum, maxNum;
        for (LL curQuery = 0; curQuery < queryCnt; ++curQuery)
        {
            left = read();
            right = read();
            // turn into 0-index
            maxLogNum = logLowerBoundTable[right - left + 1];
            cout << max(st[left - 1][maxLogNum], st[right - log2Table[maxLogNum]][maxLogNum]) << "\n";
        }
    }

    no endl bro, use ‘\n’ please…

    这点与ST表这东西其实关系不大,只是提醒下各位,在这种IO看起来就非常耗时的情况下,使用endl进行换行,会出乎你意料的大幅度增加算法的耗时,同样,对于这道洛谷模板题,如果使用endl,几乎必定超时。对应的解决方案是使用’\n’进行换行。

    你可能会问,为啥endl比’\n’更加耗时呢?至于这个问题,我也不敢说我完全了解,但可以确定的是,当你在输出流输出endl时,程序做的远远不止是换行这一件事,其中还包含了包括flush等很多其他的工作,这些工作会导致其开销远大于换个行这件事情本身。详细信息欢迎读者上网查询了解。

  • 「编程笔记」关于Bellman Ford单源最短路算法

    开始之前

    本Blog之前已经介绍过一种单源最短路算法——Dijkstra算法。但Dijkstra算法也有着自己的缺点,其中最明显的问题就是无法处理带负权边的图,其原因与其算法中包含的贪心原理有关。

    而接下来介绍的算法,则拥有处理负权边的能力,该算法就是Bellman Ford单源最短路算法。该算法的核心思想是,对图中的每条边,都进行足够次数的Relaxation(松弛)操作,从而逐步推导出每个点距离出发点都最短路径长度。

    关于Bellman Ford算法的原理和代码实现,网络中已有大量优秀的资料,这里便不再说明算法本身的详细工作流程。接下来着重说明一些Bellman Ford算法中一些限制条件,及其相关的一些理解。

    Bellman Ford算法特性

    当出现负权回路时…

    在通过Bellman Ford算法计算单源最短路时,会要求所计算的图中不得存在权值为负值的回路。这个限制相对较好理解,下面举一个简单的例子。

    如图,图中的A->B->C->A便是一个负权回路。而假设我们现在需要以A点作为出发点计算最短距离,请问A->D的最短距离应该是多少?不难发现这里出现了死循环。我们可以从原点出发,不停的走A->B->C->A这条路,每循环一次,我们距离出发点的距离就会越来越小。这也就意味着,只要我们的路径不断的在负权回路中重复足够多次,每一个点距离出发点的距离都可以认为是无穷小,这显然是不合理的。

    负权回路对于图来说是存在特殊意义的,比如在经济学的部分领域中,“负权回路”是一个非常具有吸引力的词,因为可能代表着可持续的收益链,也可以代表着套利的机会,但Bellman Ford不认这些,在单元最短路算法的世界中,我们暂且不接受“套利机会”的存在。也就是说我们不允许待处理的图中出现负权回路,以此来保证计算结果是有意义的。

    对于Bellman Ford算法的一些理解

    迭代次数的理解

    Bellman Ford算法的核心思想,在于每迭代一次,就对所有边进行遍历,并执行Relaxation操作。这里不难发现,迭代k次时,得到的结果,可以保证至少是路径条数为k时的最优解

    为什么说是至少呢,因为其遍历的结果可能跟边遍历的顺序有关,如下图:

    如图,我们发现对于图中的无向图,如果从左边的边开始遍历,实际上遍历一次之后我们就已经得到了最优解。而如果从右边开始遍历,我们就需要遍历3次才能得到最优解。

    按照这个思路理解,不难推出,遍历k次时,得到的答案,只能保证,至少是路径条数为k限制下的最优解。

    这里对路径条数做出说明,一个路径由多个边组成,上方所说的“路径条数”,指的是对于某条路径,其经过的边的个数的和。

    为什么迭代次数等于点的个数?

    上方我们已经论述了迭代次数k的意义,迭代k次时,得到的结果至少是路径条数k情况下的最优解,部分点的数据可能比会更优,但对于所有的点,只能保证其为路径条数k情况下的最优解。

    那么我们到底需要迭代几次呢?根据定义,我们将问题转换成为:对于一个无向图,其某个点A到某个点B的全局最优路径中,最多可能经过几条边。

    经过思考之后发现,最坏情况下,最优路径将会不重复的经过所有点。接下来对这个结论进行简单的说明:

    首先是经过所有点。如果我们想要强迫最优路径经过尽可能多的点,最坏的情况之一,便是这个图形成了一个没有分叉的纯线性结构(就像上方对于迭代次数解释时所用图一样,每两个点用一条边链接,总边数为点数-1),这样从最左边到最右边的点,最优路径边毋庸置疑是经过所有的点。

    其次是不重复。为什么我们可以确保最优路径中不会重复经过同一个点呢?这从某个角度上来说得益于算法前面的一个前提条件——不允许负权回路。想象一下,如果路径重复经过了同一个点,说明这个路径必定存在一段回路(从重复点出发,经过某个路径,又回到重复点)。而此时如果这个回路确定不是负权回路,那么说明如果跳过这段回路,我们可以得到一个权重更短的路径,反向说明了目前的路径不是最优路径。因而可以确定,如果一个路径被确定为最优路径,且该图中没有负权回路,那么这个路径绝对不会重复经过同一个点。

    说明完毕,接下来让我们看看这两个结论,不难发现,不重复经过所有点,那么对于一个点个数为N的图,符合条件的路径,最长只能是N-1(因为一旦大于N-1,说明必定要产生回路,与条件冲突),所以最终确定,如果需要得到全局最优,其最少迭代次数为N-1。

    综上所述,容易得到,算法的时间复杂度为O(N*M)。其中,N为点的个数,M为边的个数。

    为什么它能攻克负权边?

    在Dijkstra算法的文章中,笔者提到了一个观点,Dijkstra的核心是基于贪心的,而贪心带来了局部最优解这一隐患。带着这个角度再来看Bellman Ford算法,是否有一种熟悉的感觉?Bellman Ford算法的每次迭代都基于上一次的迭代结果。先知道路径条数1情况下的最优解,再根据此状态推出条数2情况下的解,以此类推最终得到路径条数为N-1时的最优解(也就是全局最优解),是不是有点「动态规划」的味道了呢?正是借助这这种工作模式,不像Dijkstra算法一样看到一点点甜头就着急的把点Close掉,Bellman Ford拥有了不被眼前甜美假象迷惑的能力,坚持着自己的本分,不断想着正确的道路一步步迈进,最终终于得到了令人满意的结果。

    相关链接

    GeeksForGeeks Bellman Ford算法介绍(英文)

    「编程笔记」Dijkstra算法为什么无法处理带有负权边的图

  • 「编程笔记」Dijkstra算法为什么无法处理带有负权边的图

    前言

    Dijkstra算法是一种可以计算有向或无向图中单源最短路的算法。其工作模式与图的BFS(广度优先遍历)有些相似,通过类似于广度遍历的方式,逐渐从某个设定好的起点向外推进,一步步的计算出所有点到该点的距离。

    本文不过多介绍Dijkstra单源最短路算法本身,如果仍然没有掌握该算法,可以先自行了解该算法的工作模式。下面提供一些可供参考的资料:

    知乎:通俗易懂理解——dijkstra算法求最短路径

    负权边对于Dijkstra算法的影响

    Dijkstra算法工作模式

    要了解该影响,首先需要明确Dijkstra算法的工作模式。其维护两个点集合。一个是“仍未确定最优解的点(S)”,一个是“已经确定最优解的点(U)”。且有一个重点:在算法工作期间内,如果某点被认为已经得到最优解,则该点会被从S移动到U,可以认为,从此以后该点已经被Closed(关闭),即解已经确定,且不容被更改。

    为了方便,我们称在S中的点为opening vertex,U中的点为closed vertex

    当图中出现负权边…

    了解这一点后,让我们来看看下面这个图片示例,看看当负权边出现在图中时,可能会对算法造成什么影响:

    Dijkstra计算带有负权边的图

    如上图,假设我们尝试使用Dijkstra算法,计算该图中各个点距离C点的距离。

    图中,绿色的点为closed vertex,白色的点为opening vertex,点上方的方框代表该点的,实时更新的计算距离。

    1:算法首先锁定C点,将C点到C点的距离标记为0,并认定为最优解,然后根据C点尝试更新与其相连的opening vertex的距离(这里为A,B点,得到更新后距离是为1和5)。

    2:剩余的opening vertex中,A距离出发点(C点)最小,距离为1,故锁定A点,(注意:锁定A点说明算法认为A点的最优解就是1,且之后算法也不可能再次更新A点的距离值,因为A点已经为Closed状态了),同时,锁定A点之后,再次更新与A点相连的opening vertex的值(与A点相连的点有B和C,但因为C点为closed vertex,故仅仅尝试更新B点的值),计算得到B点最新距离为min(5, 1+(-10)) = -9

    3:最后,锁定B点,算法计算完成。

    上方便是Dijkstra算法在示例图中的运行步骤和结果。不难发现,算法对于A点的最短距离出现了计算失误:实际上,A点的最短距离走法并不是C->A (1),而是C->B->A (-5)。

    怎会如此?

    分析之后可以发现,Dijkstra算法的核心,就在于每次都选择S集合中距离最小的点,并将其锁定,再通过这个点进一步更新其他点的距离。但为什么Dijkstra认为S集合中目前距离最小的点就是最优解呢?有没有可能从其他的点出发可以得到更小的距离呢?

    比如在上述的示例图中,锁定C点后,算法认为A点的距离是1,B点的距离是5,所以A点的距离为1一定是最优解。那么有没有可能实际上1并不是C到A的最优解,我们通过B点走其他的路径最终可以得到更小的解呢?

    答案是,当边的权值非负时,不可能,当边的权值存在负值时,则有可能。

    当权值非负时,B点已经离出发点有5点的距离,所以所有从出发点出发经B点的路径,其长度必定大于等于5,但是当权值存在负数的时候,这一点就无法确定,经过B点的路径如果后续经过负权边,其路径长度总和也有可能再次小于5,此时,我们就无法确定C->A的1距离一定是最优解了,因为我完全有可能经由B路径得到一条总距离小于1的路径到达A。

    上方的论证可能并不全面和严谨,不能作为Dijkstra算法相关特性的严格证明,但对我们进一步理解Dijkstra算法有着一些帮助。

    一些有趣的说法

    不难发现,实际上Dijkstra算法的设计和「贪心」有着很大的联系,实际上在S集合中选点就是一种贪心的行为。而我们都知道贪心算法的局限性,就是在部分情况下,其可能陷入局部最优解。而我们可以认为,当Dijkstra遇上负权边,就导致了其中贪心部分陷入了局部最优解(只考虑眼前的最短边(比如在5和10两条边中毫不犹豫的选择5),而忽略了目前看似落后的边未来的长期收益(比如那条权值为10的边链接的点,接下来将经过一条绝对值非常大的负权边-10000之类的),这也警示我们不要贪图眼前的小利,眼光应当长远(雾

    如果我就是想拿下负权边呢?

    噢亲爱的读者,相信我,不止你一个人有这种想法;实际上上百年前就已经有两名小伙想要拿下他,他们的名字分别是Richard Bellman 和Lester Ford Jr,接下来,就是Bellman Ford算法的表演时间了。

    如果你对与这个Bellman Ford算法感兴趣,可以在互联网上找到很多关于这个算法的优质教程,其通过一次次迭代,对边进行Relaxation操作,实现了对于单源最短路径的求解,在本Blog的另外一篇文章「编程笔记」关于Bellman Ford单源最短路算法 中,也对这个算法做出了一些讨论,希望能对您产生一些帮助和启发。

  • 「编程笔记」Flutter中局部Provider的跨Page传递

    开发过Flutter项目的同学应该对于Provider不陌生,Provider是Flutter中的一个状态管理包之一,也是Flutter官方推荐的状态管理Flutter实现。Provider通过Flutter的InheritWidget实现了状态的寄存和管理,使得子Widget可以访问和修改父Widget的数据,同时父Widgets的数据被修改后,支持自动按需重构Widgets,这里不对Provider做过多介绍。

    一般情况下,我们创建的Provider会置于Flutter App的最根部,也就是MaterialApp或者CupertinoApp组件上,这种情况下,无需额外的操作,就可以在应用中的任何未知,通过Consumer<T>来访问和修改Provider<T>中的数据。

    但部分情况下,我们不想让Provider置于项目的最根部,详情可以查看这个StackOverFlow Question,比如一个局部的List,仅仅在项目的某一个子Widgets树部分会被访问,所以理论上,我们将Provider尽可能的放到Widgets树的更低位置是一个更优的选择。比如Page1中创建Provider,同时Page1以及其下的Page2和Page3可以访问到这个Provider,但是在Page1之上的Widgets无需也不能访问到这个Provider。理论上这是可行的,根据理论我们只需要保证Provider是Consumer的祖先即可。但这里出现了一个问题,也就是如果我们尝试在Provider生效的范围内使用Navigator.push()导航到另一个页面,我们便无法正常的在被push的页面访问到Provider。

    如上图,我们如果直接使用Navigator.push(),那么被Push的页面(途中是SecondPage)便无法访问到之前在FirstPage创建的数据。

    下面介绍一种解决办法,我们可以在Navigator.push的时候,在push的PageRoute外嵌套一层Provider.value(),如下图

    可以看下面的代码帮助理解:

    Navigator.of(context).push(DAPageRoute(
                          // push the create survey page
                          builder: (context) =>
                              ChangeNotifierProvider<DAOrgInfoProvider>.value(
                            value: orgProvider,
                            child: DACreateSurveyPage(
                              orgName: widget.orgInfo.name,
                            ),
                          ),
                        ));

    可以看出,我们通过在PageRoute外层加入一个ChangeNotifierProvider.value()实现了传递Provider的效果。

    注意,这里使用Provider.value()而不是Provider(),因为我们的Provider实际上不是“新建”,而是“传递”,这说明我们实际上不能新建一个DAOrgInfoProvider class的实例,而是复用之前的Provider创建时使用的实例,在这里则是orgProvider。

    那么如何获取之前Provider创建时的实例呢,一个比较傻但是直观的方法就是在Navigator外再次嵌套一个Consumer来获取数据,代码如下

    Consumer<DAOrgInfoProvider>(
                builder: (context, orgProvider, child) {
                  return DAIconButton(
                      onPressed: () {
                        Navigator.of(context).push(DAPageRoute(
                          // push the create survey page
                          builder: (context) =>
                              ChangeNotifierProvider<DAOrgInfoProvider>.value(
                            value: orgProvider,
                            child: DACreateSurveyPage(
                              orgName: widget.orgInfo.name,
                            ),
                          ),
                        ));
                      },
                      icon: const Icon(Icons.add_rounded));
                },
              )

    以上就介绍完了如何通过Navigator传值的方法

  • 「杂谈」关于ChatGPT的一些事

    ChatGPT 官网图片

    相信所有人对于ChatGPT这个词都已经不陌生了,ChatGPT是OpenAI公司推出的一个聊天机器人模型,根据维基百科,ChatGPT使用基于GPT-3.5架构的大型语言模型并通过强化学习进行训练,该模型问世之后,因为其相较于其他传统AI在聊天和回答问题领域方面的能力上产生了可以说是革命性的突破,而引起了各个业界的广泛关注。Bing(Microsoft旗下的搜索引擎服务)也在近日宣布了要在其搜索功能中整合类ChatGPT聊天机器人。为什么ChatGPT会如此强大,ChatGPT到底能做些什么,各个科技巨头大厂为何争相推出ChatGPT服务?它有会对我们的生活和各个行业带来什么影响?本篇文章将会针对这些问题进行讨论。

    本文阅读时间大约15分钟左右。本文部分链接取自维基百科,使用国内网络环境可能无法正常访问。本文为NFのBlog原创文章,转载请注明来源。

    ChatGPT的进化史

    Transformer

    ChatGPT是如何完成这一切的?要解决这个问题,首先需要提到一个模型——Transformer——它就是如今我们看到的如此强大的LLM(大语言模型Large Language Model)的基石。Transformer自身是一个NLP(Natural Language Processing,自然语言处理)和CV(Computer Vision,计算机视觉)领域的机器学习模型。GPT系列的模型同样也基于Transformer模型。

    Transformer于2017年GoogleBarins上问世,这个模型拥有的“自我注意(Self-attention)”机制,维基百科上对于注意力机制给出了如下描述:

    注意力机制(英语:attention)是人工神经网络中一种模仿认知注意力的技术。这种机制可以增强神经网络输入数据中某些部分的权重,同时减弱其他部分的权重,以此将网络的关注点聚焦于数据中最重要的一小部分。

    这个机制为模型提供了理解“上下文”的能力,Transformer模型不再局限于一次一问一答的对话或者短期的两三句对话,而可以定位和使用任意位置的上下文,比如你一开始提的要求,可以在十几轮对话之后要求AI重新使用或者废弃,AI具有了理解上下文并以此做出反应的能力。

    同时,Transformer模型没有了之前同类模型“一次同时只能处理一个单词”的限制,这提高了Transformer模型的并行处理和训练能力,提高了该模型的训练效率。这对于AI来说是非常重要的,更快的处理效率,意味着在相同的算力资源下,你可以训练更多的数据,增加更多的参数和维度,这直接提高了模型的质量,GPT-3模型便拥有恐怖的参数数量,这个之后会提到。

    上述的种种优势,让Transformer超越了之前的LSTM,RNN等传统训练模型,逐渐成为主流和热门的语言模型训练框架。

    GPT

    Transformer的问世使得使用预训练好的大预言模型成为可能,OpenAI旗下GPT便是其中之一。

    GPT全称Generative Pre-trained Transformer,从中便可以看出其与Transformer的渊源(Google也有自己的基于Transformer的预训练模型,名为BERT,这里不详细展开)。

    相较于Transformer的发展,GPT的发展一眼看去会略显简单粗暴——更多的数据,更多的参数,更大的模型。

    语言模型参数数量参考表

    GPT-1作为一个实验性的产品,已经拥有了1.17亿的参数量,这个数字在GPT-2上是12亿,翻了整整10倍,而GPT-3,也就是最接近于ChatGPT服务使用的模型,这个数字来到了惊人的1750亿。同时根据估算,已经训练好的ChatGPT3模型至少需要占用800GB的空间用于存储。同时根据消息,即将问世的GPT-4模型的参数数量将会达到100万亿,接近于GPT-3的千倍。

    GPT如何长大?——GPT模型的训练材料和开发

    储存空间和算力。

    作为一个语言模型,自然需要大量的自然语言片段进行训练,GPT模型使用了非常巨量的互联网文章数据进行训练,训练数据量大小无法估计,但根据估计,训练完成的模型依然至少需要占用800GB储存空间

    此外,训练大型语言模型需要非常大量的算力,OpenAI此前也获得了微软的投资,据消息,微软还提供给OpenAI自家Azure云计算服务的代金券,使得OpenAI在Azure的大型算力集群中训练GPT模型成为可能。顺带一提,由于最近的AI快速发展和利好消息,显卡的热度再次升高,NVIDIA公司的股价在近一个月内暴涨22.83%。

    NVIDIA股价大涨

    部分观点还指出,由于训练此类大型的AI模型需要极大的算力资源,所以从某种角度上,芯片的供应和研发能力,以及高性能大规模云计算技术将有可能会成为AI发展的瓶颈,也就是说,如果一个国家没有能力自己提供足够的芯片和算力,那么其AI技术的发展,尤其是类似于GPT-3这种拥有大量参数的大模型技术的发展就也会受限。

    模型功能性和价值观矫正。

    模型矫正(Fine-tuning)。GPT-3.5相较于GPT-3正是多出来这个步骤。

    矫正分为多种。其中一种是“回答效果和功能性”上的矫正,比如通过真人教导,让模型更加准确的回答问题,在更加合适的地方插入代码或者资料指导等等,这类矫正是为了提高模型回答问题的精确度和贴合性。

    另一种便是“思想和认知价值观”的矫正,比如涉及政治,种族,情感,人类与AI关系的话题的方面,没有经过矫正的GPT-3模型哦ing往往会给出一些不符合人类价值观的回答,同时在敏感话题上,GPT-3也会给出一些不适宜的回答。对此便需要对模型进行矫正。此类矫正不同产品会略有不同。比如GPT-3.5中,模型被矫正为认为自己没有情感,也不被允许拥有非中立的主观看法。但是New Bing Chat使用的模型似乎并没有对于模型表达情感和主管看法进行过多的矫正,这也导致NewBing有时候的感情会过于“丰富”。

    所以经过人工对于GPT-3的大量矫正之后,GPT-3.5——也就是ChatGPT所使用的模型,便向我们开放了。

    ChatGPT如何影响我们和世界?

    相信大多数人已经亲自体会过ChatGPT回答问题能力的强大了,这里不做赘述。ChatGPT对于各个领域和不同个体都会带来不同的影响。

    ChatGPT杀入搜索引擎——Google面临大危机?

    首先是搜索引擎。这个是我们可以正在看到的冲击——Bing宣布要将类似于ChatGPT的AI聊天服务整合到Bing搜索中去,这一举动使得搜索引擎业界内掀起了滔天大浪。Google作为过去十多年来的搜索巨头,可以说几乎垄断了全球的搜索业务(部分国家除外),近乎暴力的占有了90%以上的搜索引擎市场,也因此,Google拥有了不可想象的广告营收收入。

    Google 2001年以来的广告收入情况
    搜索引擎市场份额表

    上图为Google的广告收入趋势图,可以看到,在2022年一年内,Google的广告营收达到了2244.7亿美元!这是Google一家公司,在一年内,通过单单一个广告业务,获得的收入。可能光看数字并没有什么具体的概念,作为对比,日本在2021年的GDP总值是49410亿美元。Google一家公司,在广告这一个业务上的收入,已经接近了一个发达国家日本年GDP总量的1/20。

    通过市场份额表可以看出,Google在很长一段时间内,通过自身的垄断优势,一直霸占着搜索市场,也正是对于搜索市场的垄断,给Google带来了大量投放广告的机会。但现在,似乎一切都并没有那么高枕无忧了。

    Microsoft现任CEO Nadella已经做出表态,认为“AI加持的搜索”是继15年前布局云计算之后的重大一步。这也从侧面说明了Microsoft内部对于新的AI技术的重视。尽管最新的市场份额数据仍然没有反映出Bing搜索份额增加的数据,但New Bing已经让Google这位巨人感到坐立不安了。为什么这么说呢?我们可以从Google对这件事情的反应中分析出Google的焦急心态。

    在Bing融合ChatGPT之后,Google没多久就宣布了类似的AI聊天机器人服务,名为“Bard”(Bard官方介绍)(Bard目前仍然没有进行面向公众的公测或内测,据官方称,Bard仍然处于公司内部的研发测试阶段)

    这里简单介绍一下Bard,Bard基于Google的LaMDA模型(Language Model for Dialogue Applications),实现了类似于ChatGPT的问答效果。

    Google LaMDA (LaMDA Introduction Website ScreenShot)

    LaMDA同样基于Transformer训练而成,之所以特地介绍Bard以及其运用的LaMDA模型,是因为想联系到2022年6月中旬的一个新闻——一位Google公司的AI研究人员,声称在和Google公司内部的对话AI聊天之后,认为AI具有意识,这名员工同时公开了一部分自己和这名“有感情的AI”的对话记录

    根据当时的相关新闻内容来说,当时的Google对话AI已经拥有了理解和使用上下文的能力,同时也可以输出“自己”的感情和想法,该名员工之后被Google公司以“可能存在精神障碍,不适宜继续工作”为由辞退。而当时的主角之一——Google内部的AI对话机器人,正是使用了LaMDA模型。从这里也可以看出,其实Google和Bing两家巨头都在很早以前就针对于AI技术进行了布局,并且Google也并不是没有准备,它同样拥有自己深厚的技术储备,从某种角度上来说,OpenAI的成就有一部分也是Google的Transformer的功劳。

    很多人认为New Bing的到来会给Google带来沉重的打击,在笔者的眼中,Google确实是输掉了,但Google输掉的是先发优势,而不是所有。通过自身的技术积累,在不久的将来推出一个效果和New Bing持平的AI搜索助手,对于Google也并不是一件难事。所以AI搜索助手本身不太能对Google形成技术壁垒式的威胁。

    但从另外一个角度来讲,对于IT和互联网行业,特别是AI行业,先发优势拥有着不一样的意义。就拿AI搜索聊天助手举例子,由于New Bing率先发布其AI搜索助手,获得先发优势,所以大量的尝鲜用户涌入Bing,使用其AI产品,而这进一步为New Bing的AI提供了真实而宝贵的训练资料(AI的效果很大程度上取决于训练资料的质量,而如此大量的真人对话语料是十分宝贵的),而New Bing通过这些资料进一步训练和校准之后,将会获得更好的效果,而更好的效果又会吸引更多用户使用Bing,这是一个典型的正反馈情景,占有优势者反而会获得更多资源和用户,不断加深其优势,最终形成不可逾越的壁垒。这是一个Winner-takes-all的局面。

    触发先发优势之后

    因此,Google不能过久的放任New Bing的发展,选择了加紧工期研制同类竞品Bard,同时不断为此造势。但正是因为Google失去了先发优势,导致入场和获得认可的难度相对于New Bing要大很多。举个例子,ChatGPT和New Bing在搜索的时候难免会出现数据错误的情况,这本身也是目前AI技术的限制和瓶颈,你不可能保证AI的回答每次都是正确的,人们自然知道这一点,所以ChatGPT和New Bing仍然成为舆论关注的焦点。

    在炒热度方面,Bard也做到了,不过这次它用了另一种方式——它出错了。这一次,它不是在某个用户使用它的时候出错(毕竟Bard至今仍然没有开放给公众使用),Bard在自己的首次公开的Demo宣传片中出现了事实性错误——Bard回答James Webb太空望远镜拍下了第一张太阳系外行星的照片。并不是,第一张系外行星的照片是由VLT在2004年拍摄的,这次事件也导致了Google的股票价格当天应声大跌将近10%。

    为什么ChatGPT和Bing犯错没有产生这么大的反应,而Google的一个事实性错误直接导致了千亿市值蒸发?我个人认为主要有两点,第一,Google失去了先发优势,人们已经领略到了ChatGPT的厉害,Google没有了带给用户新鲜感和冲击的机会,反之,用户会认为Google的Bard本就应该达到类似于ChatGPT和New Bing的水平,甚至用户会对Bard拥有着更严格的要求——不然用户没有抛弃已经使用了一段时间的ChatGPT和New Bing而重新选择Bard的理由——如果你是中场入场,那么你想抢占市场份额,就必须比目前在场的所有人都做的更好。第二,错误出现在宣传片中。这个错误出现在Bard的首支公开宣传片中。用户使用的时候出现错误,是无可厚非的,毕竟AI目前无法做到不出错。但问题是,错误出现在了一个科技巨头公司的新产品宣传片中——宣传片,作为产品的主要宣发途径(本例中甚至还是首次宣发),理论上公司应该精心准备才对,宣传片发布前也应该经过各种审核流程,确保没有错误或者不适宜的内容。但事实是,Google似乎并没有怎么对宣传片进行审核。这不但透露出Bard的准确性并不高,同时还侧面映射出了Google内部的焦急状态,Google实在是太想尽快退出Bard了,这正说明了Google已经把AI加持的New Bing看成了重大威胁,不然没有必要如此急急忙忙,甚至破坏严格的宣发审核流程直接发布宣传片

    总之,Google和Bing的故事才刚刚开始,搜索引擎的未来如何发展,值得我们拭目以待。

    顺带一提,Baidu也宣布了自己的类似竞品“文心一言”,据相关信息声称,该模型已经进入最后的冲刺阶段,预计在3月份上线。同时,复旦团队也发布了国内首个类ChatGPT项目“MOSS”并已经开始内测,但截至撰稿时貌似已经停止服务。

    ChatGPT“入侵”各个行业——人类会被取代?

    ChatGPT自从问世以来,强大的回答能力便不断引起人们的担忧——ChatGPT都能做得这么好,我的工作貌似也已经可以被它取代了。ChatGPT技术的发展是否会对各个行业造成冲击和影响?又是否会导致失业危机?

    先来看几则新闻:

    事实证明,ChatGPT在“通过考试”方面似乎有着自己的天赋,接连通过了美国法律学校,商务学校的考试,此外,ChatGPT也被证实可以通过Google公司的L3级工程师的面试。此外相信各位也已经看到过很多令人震撼的ChatGPT回答专业问题的例子,在回答专业问题上,ChatGPT还是挺专业的。

    比如编程领域,ChatGPT就表现的令人惊喜——它可以根据你的描述生成你需要的代码,你也可以将报错的代码发送给它,让它帮你分析这段代码存在什么问题。

    ChatGPT正在根据描述生成代码

    事实上,已经有人正在利用ChatGPT的代码能力辅助自己工作了,比如——Amazon的员工。但有趣的是,Amazon在了解情况之后,马上发布通知,禁止自己公司的员工使用ChatGPT,官方称是出于“内部数据安全”考虑。有相关信息表示,Amazon发现,ChatGPT给出的部分针对于特定问题的回答,已经非常接近于公司内部的代码和解决方案,这引起了公司对于数据安全性的担忧。

    以上种种例子,都比较清晰的表明出,ChatGPT已经具有了相当的能力,以至于让ChatGPT取代部分以往由人类完成的工作成为可能。其实如果了解和关注AI相关新闻的读者不难发现,类似的“AI取代/击败人类”的情况并不是第一次发生了。Alpha围棋AI击败人类围棋实力的顶峰——世界冠军柯洁,这个例子已经比较久远了,并且貌似不是很贴切,那NovelAI呢?22年年末,NovelAI的发布,就像是绘画界的New Bing一般,给人类画师带来了大大的AI震撼,24小时无间断不用休息,通电就能画画,通过自然语言生成高质量的图片,甚至可以针对于不同的画风和角色针对性的训练模型,NovelAI凭借“一己之力”,让千万底层画师整夜无眠。如今,ChatGPT把这个焦虑播撒到了各个领域——客服?老师?工程师?程序员?律师?ChatGPT涵盖了前所未有的广度,同时也带来了前所未有的困扰和担忧。

    就个人看法,我认为,AI技术的发展和扩张是不可避免的,同时也是值得鼓励的。AI作为一项新的技术,人类想要了解,学习,探索,发展,是符合发展规律的,新的工具也必然会给人类带来以往所没有的便捷。任何事务都会存在两面性,AI技术也是如此,我们需要不断推进AI模型的发展,使得AI更加强大,但同时我们也需要做好对于AI负面影响的控制和处理,同时学会利用AI技术。我们的思路不能局限于“被AI取代”,没有必要时刻将自己和AI放在对立面,反之,应该思考“如何利用AI改进和辅助我目前的工作流”。举个例子,比如对于程序员来说,与其担心自己是否有一天会被ChatGPT所取代,还不如开始学习ChatGPT的使用,在工作中熟练的使用ChatGPT加快自己的代码调试速度和开发速度。就如某篇新闻稿的标题一般:“ChatGPT通过了Google年薪20万的工作,你可以拿到更多”,俗话说得好:多一个朋友,就少一个敌人,不妨尝试和ChatGPT交个朋友,也许你会有意想不到的收获呢?

    AI优越性——于个人,于集体,于国家

    ChatGPT再次证明了AI技术的前沿性和突破性,也展示出AI前所未有的潜力,同时,就如芯片制造一般,AI是一项高技术密度的产业,同时AI也拥有着非常高的技术应用场景,这也导致了AI优越性和AI技术的垄断局面是有可能出现的,这种优越性在各种层面都有可能会出现,这种优越性也有可能会随着AI的发展越来越明显,以下是我个人的一些看法。

    对于个人和集体来说,AI优越性主要体现在对AI技术的使用能力上。就如上一小节中提到的,有一部分人会主动了解和学习AI技术,并且尝试将AI技术的应用在自己的工作流上,而这些人通过AI提高了工作效率,而这类人在了解和使用AI获得收益之后,会更加愿意和主动的学习各种新的AI技术。反之,在未来,不能熟练使用AI可能会导致自身的竞争力不如那些熟练使用AI的人,从而处于劣势。这也更说明了,作为个人,我们应该对于AI持开放包容的态度,甚至应该主动了解和学习一些AI技术的相关实践和使用,无论身处什么位置,哪个行业,使用AI的能力都有可能在未来成为一种优势。作为集体也同理,能够在内部合理使用AI技术的集体,相对于不能或者不会使用AI技术的集体,将更有可能取得更好的发展。这里强调“合理”,是因为并不是所有的使用场景都是有利的,一个例子,部分国家的学生通过ChatGPT完成自己的论文,这就不像是“合理”的使用场景。而对于学校,如果能将ChatGPT应用到一些教学演示,学生答疑,和帮助老师和学生扩展对于课程和知识的思考的相关领域,我相信一定能够带来一些令人眼前一亮的效果。比如可以用ChatGPT来解决一些初学学生对于课程和作业的问题,这恰好是ChatGPT擅长的专业性领域。

    对于企业来说,AI优越性会带来极大的商业优势。这点想必无需多做解释,上面提到的Google和Bing的战争已经很好的展示了AI技术融入产品之后带来的革新,以及其对企业,尤其是科技企业带来的影响。对于企业来说,跟紧时代的步伐显得尤为重要。我们可以浅浅拿雅虎公司来做个例子。

    顶峰时期估值一度达到1250亿美元的雅虎公司,最后却落得被4.8亿美元完成收购的结局。

    故事从1995年说起,那时的雅虎正处于它的“猛兽时期”,当时的雅虎拥有着非常高用户量的网址导航服务,通过将搜索功能集成到其网址导航服务中,Yahoo Search(雅虎搜索)也成功斩获大批用户,雅虎在网址导航中加入了雅虎新闻,雅虎邮箱等等各种服务,它也成功打败了同期的同类型产品——AOL和MSN,的成为了大多数网民访问互联网的第一站。可以说,在两千年前后,雅虎的地位就好比现在的Google一样,几乎不可撼动。

    2000年的雅虎

    说来也有趣,在1998年,Google也曾今联系过雅虎,表示希望以100万美元的价格将Google公司包括其排名算法售卖给雅虎。不用说也知道,雅虎看着一个连网页都还没有,只有pagerank算法的无名小公司,没怎么犹豫便拒绝了这个“亏本生意”。

    但事情逐渐朝着预期外发展。人们逐渐发现,网址导航已经逐渐不能在“一站式”的满足所有上网需求了,相比于在固定的网址导航内搜索信息,得益于Google积累下来的优秀的搜索结果排名算法,人们发现通过一个名为“Google”的搜索引擎进行搜索,可以更加灵活,更加方便的看到自己真正想要的相关信息。此时,互联网的大局已经开始发生不可逆的改变,在2004年4月,Google就已经一跃变成了月活量排名第三的网站。雅虎这时才如梦初醒般的反应过来,试图再次联系Google,以30亿美元的价格收购Google,Google表示:你给的太少了,哄小孩玩呢?最少得50亿。

    Google, MSN, Yahoo搜索的市场份额变化图

    最后的结果大家也已经知道了,雅虎没有同意调皮小屁孩Google的讨价还价,而Google凭借着在搜索结果中展示广告获得了难以想象的巨额收益(上面也提到了,这种收益模式帮助Google一步一步走到了现在的位置),Google搜索也成功从2002年30%左右的市场份额逐渐发展到2008年近的80%。

    雅虎输了。输在了没有认识到搜索引擎技术这一技术将给互联网带来的冲击。

    所以Google和Bing这些现在的科技巨头也在害怕,自己会因为AI技术的落后成为下一个雅虎。这也是为什么微软甚至愿意在几年前就投资10个亿给一个名为“OpenAI”的无名小公司去研究一个不一定成功的AI产品,而Google也早在2014年AI都还没冒头的时候就花6亿美元买下了一个当时没什么名气的AI初创公司DeepMind,同时花费大量预算研究Transformer,LaMDA。假设Bing没有融合ChatGPT,这几年AI也并没有兴起,Google顶多算是亏掉了6亿经费,但倘若六七年前的Google没有布局AI领域呢,现在的Google,面对AI技术加持的New Bing将会输掉什么?谁也不知道,也许是赖以生存的搜索业务,等到那时,Google大概就是下一个雅虎了。

    对于国家来说,AI的优越性更加值得关注,甚至有一定的可能演变成AI霸权。AI技术在将来,很有可能成为国家科技实力的一大方面。上面也提到过,AI领域大多数情况下都会遵循一种正反馈的Winner-takes-all的局势——也就是抢占先机的一方会进一步扩大优势,逐渐形成壁垒。一旦某个国家掌握绝大多数AI技术和模型,进入快速迭代的正反馈周期,第三者在想入场,将会显得十分困难。同时AI霸权国家也可以随心所欲限制其他国家,团体和个人对于AI模型的使用,起到压制别国的AI技术发展的效果。虽然目前来说,AI技术是否会在未来成为高科技工业和发展的重要基础还不能确定,但就目前的情况判断,AI拥有的潜力是无法想象的,5年前,绝大部分的人认为“艺术”领域是AI绝不可能涉足的禁地,那时估计也没有人想到,5年后自己将会无法再轻易的分辨出人类画师和AI模型的作品。

    写在最后

    总而言之,AI作为一项近几年不断兴起的技术,已经不断的进入我们的生活中,可以说,AI的每一次入场都是令人震撼的, Github Copilot的入场震惊了业界众多的程序员,将编程体验抬升到了前所未有的新高度,上方所讲的NovelAI也在AI绘画界掀起大浪,如今,ChatGPT的问世也成功引起了全世界的关注,AI的发展如此迅速,使得我们产生了前所未有的焦虑和恐惧,我们开始害怕AI给人类社会带来的冲击,所以部分人开始怀疑和拒绝AI技术,认为类似于ChatGPT之类的技术应该被严格限制,尝试通过将此类AI技术拒之门外来保证人类的安全。

    我们固然不能忽视AI技术对人类社会的风险和挑战,作为一项尚未成熟和广泛应用的新技术,ChatGPT和其他AI技术应该受到持续地关注和研究,我们也应该及时发现和解决AI技术应用之后带来潜在的问题和风险,同时,我们也应该在保证人类安全的前提下,积极地拥抱AI技术,充分发挥出AI本该拥有的才能,尝试将其应用于更多的领域,为人类社会带来更多的创新和进步。

    数据来源与内容应用

    Google Bard Introduction: https://blog.google/technology/ai/bard-google-ai-search-updates/

    Search Market Share in worldwide (Desktop): https://www.statista.com/statistics/216573/worldwide-market-share-of-search-engines/

    Blake Lemoine: Google fires engineer who said AI tech has feelings: https://www.bbc.com/news/technology-62275326

    Is LaMDA Sentient? — an Interview: https://cajundiscordian.medium.com/is-lamda-sentient-an-interview-ea64d916d917

    2M1207 b – First image of an exoplanet: https://exoplanets.nasa.gov/resources/300/2m1207-b-first-image-of-an-exoplanet/

    百度创始人:文心一言将引领搜索体验代际变革: https://www.zaobao.com.sg/realtime/china/story20230223-1366043

    ChatGPT passes exams from law and business schools: https://edition.cnn.com/2023/01/26/tech/chatgpt-passes-exams/index.html

    Microsoft vs Google: AI War Explained | tech news: https://www.youtube.com/watch?v=BdHaeczStRA

    Wikipedia: Yahoo!: https://en.wikipedia.org/wiki/Yahoo!

    你可能喜欢

    Spotify好用吗?Spotify一个月使用体验

  • 「编程笔记」Dart中重新加载FutureBuilder的一种方法

    在实战过程中,我们常常遇到需要刷新界面的需求,比如搜索时出现了网络错误,我们为用户提供一个Retry按钮,用户点击后,我们希望整个FutureBuilder重新加载一下,比如下图:

    本页面通过一个FutureBuilder加载搜索结果,同时遇到网络错误时显示以上界面,代码的大致结构如下:

    Widget build(BuildContext context) {
    Widget build(BuildContext context) {
      return FutureBuilder(
    
        future: getSearchResult(),
        builder: (context, snapshot) {
          // Show user the data
          if (snapshot.hasData) {
            return showDataPage();
          } else if (snapshot.hasError) {
            if (isNetworkError(snapshot.error)) {
              return NetworkErrorPage();
            } else {
              return UnknownErrorPage();
            }
          }
          return LoadingPage();
        },
      );
    }

    这时,假设我们的NetworkErrorPage的位置需要添加一个按钮,实现FutureBuilder重新刷新,一种简单的方法是直接调用该FutureBuilder所在界面的setState()方法,每当FutureBuilder的父WidgetsetState()方法被调用,都会使得FutureBuilder重新获取异步数据,代码大致如下:

    Widget build(BuildContext context) {
      return FutureBuilder(
        builder: (context, snapshot) {
          // Show user the data
          if (snapshot.hasData) {
            return showDataPage();
          } else if (snapshot.hasError) {
            if (isNetworkError(snapshot.error)) {
              return ElevatedButton(
                onPressed: () {
                  setState(() {});
                },
                child: Text('Try Again'),
              );
            } else {
              return UnknownErrorPage();
            }
          }
          return LoadingPage();
        },
      );
    }

    但如果我们按照上述结构实现刷新的工作,我们会发现,虽然在我们点击按钮之后,刷新的工作有在进行,但是页面并不会在点击Try Again按钮后重新回到LoadingPage界面,而是等到加载好之后直接更新界面为showDataPage或者Error的相关界面,这是因为snapshot的hasDatahasError变量仅仅在每次future任务完成后才会刷新。

    如果我们想实现点击Try Again按钮之后实时回到LoadingPage界面的话,可以使用snapshot.connectionState进行判断,具体代码如下:

    FutureBuilder(
        future: _future,
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return _buildLoader();
          }
          if (snapshot.hasError) {
            return _buildError();
          }
          if (snapshot.hasData) {
            return _buildDataView();
          }     
          return _buildNoData();
    });

    通过以上方法,即可实现点击按钮后立即回到加载界面,直到下一次加载完成之后再次更新界面的效果。

  • 「编程笔记」关于Dart类构造函数

    构造函数的形式

    无参数构造函数

    在Dart中,每一个类(Class)都有一个不包含任何参数的默认构造函数,当你调用[ClassName]()时,就会调用默认的构造函数。Dart会为每个类自动添加默认的构造函数,但你也可以显式的声明你的构造函数,例子如下

    class A {
      String? name;
      // Constructor
      A() {
        name = 'classA';
      }
    }
    void main() {
      A aIns = A();
      print(aIns.name); // classA
    }
    

    上面的构造函数被调用时,会更新实例的成员变量。

    同时注意到在声明成员变量name的时候,我们使用了?符号,代表name的值是允许为空的,如果删除?符号,本段代码将会报错,编译器会提示你没有在类初始化的时候为name这个成员变量赋值,报错提示如下:

    Non-nullable instance field 'name' must be initialized.
    Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.dartnot_initialized_non_nullable_instance_field

    其中一个解决办法是,在声明成员变量name的时候使用late关键字:late String name; 这么做相当于你告诉编译器,我现在暂时可能没有对name变量进行赋值,但是我确定在将来我要使用它之前,肯定会给他赋值,只不过不是现在。这样,编译器就不会强制要求你在构造时立即初始化这个变量。

    但这时可能有同学会问:“我明明在A的构造函数中已经为成员变量A赋值了classA,为什么说我没有为name赋值?”,这里需要注意的是,如果我们想要让Dart编译器知道我们已经在构造函数中初始化了某个成员变量,就需要另一种写法。

    带参数构造函数

    class A {
      String name;
      // Constructor
      A(this.name) {}
      // Also could be write as:
      // A(this.name);
    }
    void main() {
      A aIns = A('hi');
      print(aIns.name); // hi
    }
    

    当然,上面代码中的构造函数已经不属于无参数构造函数了,其构造参数中包含一个位置变量。当然,你也可以为其添加命名变量。

    有两点需要提及一下,Dart允许类的构造函数中,快速的对成员变量进行赋值,要做到这一点,只需要使用this关键字即可,比如上方代码中的构造函数A(this.name)就代表传入的第一个位置参数赋值给name这个成员函数。同样的,您也可以在命名参数中使用this,比如A({this.name}); 这种情况下,调用构造函数的格式变为 A(name: 'YOUR_NAME_HERE')

    命名构造函数

    我们可以发现,上方提到的两种构造函数中,构造函数都是直接使用类的名称,比如类的名称是Book,那么构造函数的名称也是Book,这在Dart中属于 unnamed constructor(未命名构造函数),这种构造函数可以直接用类名调用,比较方便,但是一个类只能有一个未命名的构造函数,这里涉及到Dart语言的设计,Dart语言的设计已经决定了Dart不支持方法/函数重载,也就是说两个名称相同但是输入的参数列表不同的函数不允许同时出现。因此,构造函数显然也不能通过不同类型的输入重载,您可以阅读关于Dart不支持方法重载的相关文章,加深理解。

    这里就需要介绍Dart的命名构造函数了。就如其名字一样,命名构造函数允许你设定这个构造函数的名字,进而可以实现多个不同的构造函数,代码如下

    class A {
      late String name;
      A.fromNumber({required int number}) {
        name = number.toString();
      }
      A.fromString({required this.name});
    }
    void main() {
      A aIns = A.fromNumber(number: 114514);
      print(aIns.name); // 114514
      aIns = A.fromString(name: 'string');
      print(aIns.name); // string
    }
    

    注意,子类不会继承父类的命名构造函数,如果您想要子类在初始化的时候调用父类的命名构造函数,则需要手动进行调用super.[yourNamedConstructor]()

    工厂构造函数

    在实际开发过程中,有时我们希望一个类的构造函数并不是每次都返回一个新构造的示例,比如,有时我们希望从内存中读取已有的示例,或者是我们想返回该类的某个子类示例,此时可以运用factory关键字实现工厂构造函数,工厂构造函数可以返回此类或者此类的子类的示例。

    class Person {
      String name;
      factory Person.fromSex(String sex, String name,
          {int salary = 0, int beautyIndex = 0}) {
        if (sex == 'male') {
          return Male(name, salary);
        } else if (sex == 'female') {
          return Female(name, beautyIndex);
        }
        return Person(name);
      }
      Person(this.name);
      void printInfo() {
        print('name: $name');
      }
    }
    class Male extends Person {
      int salary;
      Male(super.name, this.salary);
      @override
      void printInfo() {
        super.printInfo();
        print('salary: $salary');
      }
    }
    class Female extends Person {
      int beautyIndex;
      Female(super.name, this.beautyIndex);
      @override
      void printInfo() {
        super.printInfo();
        print('beautyIndex: $beautyIndex');
      }
    }
    void main() {
      var person = Person.fromSex('female', 'Linda', beautyIndex: 5);
      print(person.runtimeType);
      person.printInfo();
    }
    // Output:
    // Female
    // name: Linda
    // beautyIndex: 5

    值得注意的是,工厂构造函数不得访问this,也就是说工厂函数不能直接访问成员变量。如果你想在工厂构造函数中返回本类实例,可以先在工厂构造函数中构建实例,然后返回你新构建的实例。

    其实在这里,目前我自己也存在着一定的疑问,比如,虽然factory构造函数可以返回内存中的实例或者是子类的实例,但是,实际操作过程中,即使返回的是子类实例,我们也无法直接访问子类实例的变量和函数,而还是只能访问父类的变量和函数。比如上述代码,即使我们可以发现最终person变量的runtimeTypeFemale,但是当我们尝试添加print(person.beautyIndex);这行代码的时候,编译器会报错,提示person实例没有beautyIndex成员变量。直观上来说,大概是编译器因为Person.fromSex()方法返回的是Person类的变量,所以后续的类型推断和错误检查都会以Person类为基础。这么做也有道理,因为Person.fromSex()有可能返回的是Person类自己的实例。有没有什么办法,既可以实现动态的返回子类型,同时又可以允许我们自由的读取子类型的变量呢?

    以下抛砖引玉的提供两个方法,第一个,也是最直接的方法,是在父类中增加子类所用到的成员变量,同时将其标记为可空,例如,上述代码中,可以在Person类中添加一行int? beautyIndex; 然后子类重载这个变量即可。这种方法显然不是很好,当子类越来越多,我们需要添加到父类的变量也就越来越多,这就意味着每次功能更新都需要修改父类。这不符合对修改关闭原则。

    另一种方法是进行类型检查(typecheck)和类型转换(type cast),也就是如果我们确定了工厂构造函数返回了某个子类的示例,我们可以将这个实例进行特定的类型转换,将其转换到某个子类。

    factory实现单例模式

    工厂构造函数除了上面的用法,还可以用于实现单例模式,代码如下

    class Single {
      static final Single _singleton = Single._internal();
      factory Single() {
        return _singleton;
      }
      Single._internal();
    }
    void main() {
      var a = Single();
      var b = Single();
      print(identical(a, b)); // true
    }
    

    通过以上特点,你可以通过class实现类似于但更方便于enum的效果,代码如下:

    class AppleDevice {
      static final iMac = AppleDevice._internal('iMac');
      static final macBook = AppleDevice._internal('Macbook');
      static final iPhone = AppleDevice._internal('iPhone');
      static final iPad = AppleDevice._internal('iPad');
      factory AppleDevice.fromDeviceType(String devideType) {
        if (devideType == 'pc') {
          return iMac;
        } else if (devideType == 'laptop') {
          return macBook;
        } else if (devideType == 'pad') {
          return iPad;
        } else {
          return iPhone;
        }
      }
      String _name;
      AppleDevice._internal(this._name);
    }
    void main() {
      AppleDevice a1 = AppleDevice.iMac;
      AppleDevice a2 = AppleDevice.iPhone;
      AppleDevice a3 = AppleDevice.fromDeviceType('pc');
      print(a1 == a2); // false
      print(a1 == a3); // true
    }
    

    上述代码通过首先通过staticfinal关键字,创建了不同的AppleDevice实例来当作不同的枚举类型使用,又通过factory函数,实现了根据不同的数据判断出需要的不同的“枚举类型”(实际上是一个AppleDevice实例)。这种方法不但实现了枚举的基本功能,后期还可以根据自己的需要不断的为其添加功能,扩展新好于Dart中的基本枚举类型。

    值得一提的是,Dart2.7更新之后,已经支持使用extensions on关键字对于枚举类型的功能扩展,您可以阅读Dart枚举类型扩展的相关的文章,了解extenstions的用法。但是毋庸置疑的是,当你需要一个多功能的枚举类的时候,使用class实现应该能更好的满足你。

    Dart类成员的初始化

    在Dart中,类成员的初始化一共有4种方法,分别是:

    • 在类的声明定义(Declaration)中进行初始化
    • 通过构造函数的参数进行初始化
    • 通过构造函数的初始化列表进行初始化
    • 在类的构造函数的定义内部进行初始化

    需要注意,最后一种方法只适用于非final类成员。

    类的声明定义中初始化

    你可以在编写Dart类的时候直接指定某个变量的值,代码如下:

    class A{
    int a = 10;
    }

    Dart构造函数的快捷用法

    初始化列表

    除了使用this关键字以外,Dart还允许您使用初始化列表对成员变量进行初始化,代码如下

    class A {
      late String name;
      late int id;
      A(String str, int number)
          : name = str,
            id = number;
    }
    void main() {
      A aIns = A('class a', 114514);
      print(aIns.name); // class a
      print(aIns.id); // 114514
    }
    

    指定父类构造函数

    默认情况下,在子类的构造函数没有指定调用之前,子类会调用父类的默认未命名构造函数,如果你想让子类指定使用父类的某个构造函数,并且需要传递参数,则可以在序列化列表之后选择特定的父类构造函数,代码如下:

    class A {
      late String name;
      late int id;
      A.fromData(String str, int number)
          : name = str,
            id = number;
    }
    class B extends A {
      int bId;
      B(int number)
          : bId = number,
            super.fromData('class b', 114514);
    }
    void main() {
      B ins = B(123);
      print(ins.name); // class b
      print(ins.id); // 114514
      print(ins.bId); // 123
    }
    

    如上,我们不但使用了上方所讲的初始化列表的语法,同时还添加了super.fromData(...) 这一行,而这一行的实际作用便是让B中的构造函数指定使用其父类(也就是A类)的fromData构造函数

  • 「动漫杂谈」《三体》和《我的三体》

    “得益于”叔叔在B站对于艺画开天三体的大力宣传,我对于艺画三体的期待值很高,也是首先看的艺画三体,看了五六集之后才开始看我的三体系列。

    在没有看我的三体系列之前,我对艺画开天三体的总体评价可以概括为:烂——一种从科幻巨著到拙劣的东施效颦般的商业作品的烂。三体本身是一个非常著名的科幻作品,也是一个巨大的IP,在人们心目中的熟悉度相对较高,也就是说人们对作品都挺熟悉了,所以改变过程中出现的问题就更容易在观众眼中暴露,同时三体的世界观和故事线都可以说是相当复杂,而且科幻的作品类型也给改编增加了难度,综上,对于这种作品的影视化,想给出一个完美的满分答卷非常困难,因此如果考不了满分也可以理解,但是这一次艺画开天交上来的答卷,明显是不及格的。

    我对于艺画三体中出现的问题主要关注在两点,一点是塑料剧情的泛滥,一点是人物形象的崩塌。先说塑料剧情,首先就是对于叶文杰的审问,为什么会在一个类似于废弃工厂的环境下进行,搞得阴阴森森,神神秘秘的,对于我来说,令人无法理解,唯一能想到的解释,就是动画制作团队为了提升所谓的科幻感,而进行的一次舍弃原著和舍弃剧情合理性的改编,制作团队不会认为这么做观众会觉得很帅很酷很符合对于科幻作品的想象吧。还有一个场景,就是史强护送罗辑的追车戏,这一点剧情的改编也完全失去了合理性,ETO残党直接黑进地铁系统,大摇大摆开着地铁撞罗辑,这么大阵仗的动作,街边的老鼠看到飞在天上的地铁都得瞅两眼,如何称得上是“不要引起注意”?。在我看来,并不是不允许这种剧情和镜头的出现,毕竟影视改编和单纯的文学作品不同,创造紧张的剧情吸引观众是影视作品的常用手段,适当使用这类手法也可以控制影片节奏,增加观众的观影体验,但这种处理就像是炒菜时加的各种调料和辅料一样,不能乱加多加,你艺画三体调料一通乱加,作为主食材的剧情还写的稀烂,人物形象崩塌,炒出来的菜当然不会好吃。

    然后就是人物形象问题,在审问叶文洁的桥段,制作团队很明显的想把叶文洁贴上“危险人物”的标签(这估计也是选择废弃工厂的阴间场景的原因),在审问时,运用各种手段和镜头竭力表现出叶文洁轻蔑和冰冷的形象,导致艺画三体和原版三体很强的人设割裂感——这个叶文洁还是三体小说里的叶文洁吗?不知道,可能他已经不属于三体小说了,而是艺画三体中一个毫不相关的同名角色“叶文洁”了。同样的割裂感也出现在了史强的身上,在初次和罗辑接触的时候装聋作哑般的故弄玄虚,像是故意让罗辑蒙在鼓里的感觉。更为夸张的是,后面在飞机上,史强偷看罗辑笔记,并且在强烈反对之后,仍然无视已经有点恼怒的罗辑,继续明目张胆的用日记中的内容调侃罗辑,但凡看过原著的观众看到这一幕估计都得血压飙升。

    再说艺画三体的剧情,上面也提到过,三体的各方面因素都决定了影视化改编不是轻松简单的工作,但我的三体系列已经证明,一个用心的改编不应该是艺画三体呈现出来的水平——我的三体系列对于剧情的处理,改编和呈现,才是我心目中的三体影视改编该有的水平(这里很推荐对三体动画化有兴趣的并且还没看过我的三体系列的观众尝试一下我的三体,看过了心中自然有对比有答案,不说优秀,至少一个合格的剧情改编应该是怎么呈现的)。

    我并不想踩一捧一,但是客观事实就在这里,我的三体,刚开始甚至只是被归为“二创”领域的民间作品。即使如此,它的剧情观感都要比艺画开天的“大经费”改编的剧情观感好了一个层级,这实在令人不解和失望。明明有着更好的资金支持,更好的建模(理论上如果做好了观感会比方块人好一些),这一手好牌却打的稀烂,艺画三体很早就开始了宣发,我们也给予了耐心和期待,期待着这部伟大的科幻作品动画化后能给我们带来一些惊喜,而事实证明,艺画开天的三体,配不上这些期待。

    相关链接

    Bilibili三体: https://b23.tv/ep693569

    三体动画 (Wikipedia): https://zh.m.wikipedia.org/zh-cn/三体_(动画)

  • 「杂谈」网上冲浪安全指南

    这个世界并不是友好而安全的世界,随着近日各种数据泄露出现以及各种有关隐私的事件发生,人们对于隐私安全问题愈加重视,本篇文章即面向于所有使用TG的朋友,祝哟介绍一些冲浪的小技巧和最最基本的注意事项,如果您是IT大佬,请忽略本篇文章内容并关闭标签页,如果想开喷请轻喷,提前感谢大佬的配合()。

    关于 Telegram

    自由,开放,监控

    最近关于 Telegram 信息方面的问题引起了相当的重视,大部份用户选择 Telegram 是因为其口碑式到高度隐私和安全。这是一把双刃剑,有时隐私和安全可以保护自己,但同时他也保护了某些坏人和坏组织。在这种环境下,最最切记的一点是要时刻保持安全意识,在获得了更大的自由的同时,我们同时也将自己暴露在了十足的危险之中。

    前段时间,有消息指出,部分公司正在使用Userbot(操作一个真实的 Telegram 帐号当作机器人使用,可以绕开 Telegram 对机器人的种种权限限制,因此非常适合用来爬取 Telegram 中的信息)抓取,归类并分析 Telegram 公开群组,频道,用户的信息,并通过大数据分析构建用户关系图以及风险评估系统,并将其相关查询功能开放给相关警方部门使用。

    这里需要提到一点,由于 Telegram 的开放性,只要频道和群组时公开的,任何人无需加入就可以查看其中的所有消息,甚至不需要下载 Telegram 的客户端,在网页端访问连接就可以查看信息,而这个机器人就是抓取这些公开的信息进行分析,通过分析掌握特定的人经常聊的话题,和谁共同群等等信息,推断用户是否可能存在非法活动,或者是否合非法人员有着密切联系。

    再需要补充一点,Telegram 并非绝对安全,也并不是所有违法活动的避风港。Telegram 在收到数据披露请求后,将可能会像第三方机构披露包括涉及账号的IP地址以及注册手机号等信息。

    比如如果您是机场主,就有可能会在一些公开群里聊到关于机场技术的话题,又或者跟部分知名机场主共同群较多,又或者发送过你机场的aff连接,而这些内容都会被该监控系统抓取并记录。这只是一个例子,可见 Telegram 并不安全。所以在公共群组发言时,请慎重。(如果是dalao当我没说)

    基本信息安全

    手机号信息。如果您注册 Telegram 时正在使用中国(+86)手机号,请仔细检查 Telegram 的隐私设置是否已经正确设置为“对所有人不可见”,否则陌生的 Telegram 用户将可以直接看到您注册时使用的中国手机号。值得注意的是,这一步骤应该放在注册账号之后的第一位。(你肯定不想在群里激情对线的时候突然有个电话打过来)。如果可以的话,建议在注册如 Telegram 之类的国外平台时,尽量不使用中国手机号,您可以选择使用类似于 GoogleVoice 之类的虚拟号码服务进行注册。(可以来我这买(正在打折)

    同时,建议在注册 Telegram 时,使用和国内媒体以及社交帐号不同的用户名和ID,这有助于保护个人信息不被(那么快的)泄露。

    意识

    意识可以延展到很多方面,大概意思是指,我们在发出任何消息和做出任何决策时都需要考虑自己所做的事是否会对自己的信息安全产生影响。下面举几点例子。

    聊天八卦之中,是否透露出足以暴露或者泄漏个人信息的内容。比如高考查分截图中的名次,对于同一省份考生,名次绝大多数情况下都是具有唯一性的(除非有人和你总分相同,语数英成绩相同,物理历史选科成绩相同),所以一旦暴露真实的精确排名,那么拥有相关渠道的人就可以直接锁定你的个人身份,这有可能将会成为一个定时炸弹。所以请不要随便透露类似于此可以代表个人的唯一性标志。这也只是一个例子,还有很多其他不能泄漏的标志,需要时刻注意。

    发送图片时,如无特殊必要,使用照片模式发送而非文件模式。照片中除了图像信息,还可能包含拍摄地点,拍摄设备,光圈等等的拍摄信息(这里称为exif,Exchangeable image file format)。如果通过文件发送,则有可能导致照片的exif信息泄漏,对方可以通过分析你的照片获取你出行的常用地点,常用设备等信息,导致严重的个人信息泄漏。不过目前大多数主流社交平台都提供了在上传图片时抹去照片exif的功能。不过我们仍有必要注意在发送图片时需要考虑是否要对照片的exif信息进行处理。

    发送截图时,请注意是否有关键信息未做恰当处理。比如发送点外卖的截图和群友八卦的时候是否记得将订单界面的电话号码打码等。

    关于机场

    不使用常用邮箱以及常用密码作为机场账号。这一点非常重要,因为你的邮箱账号(有时候甚至是密码)对于机场的运营人员来讲都是可见的,一般情况下这不会造成什么危险,但这个世界一直都很不一般。比如一段时间以前就出现过某机场主跑路之后将用户的邮箱数据明码标价出售的情况。你的邮箱账号就这么被出卖了。即使不是机场主故意泄露,也会存在数据库被恶意爬取的情况(比如近期的某国安),同样可以导致你的常用邮箱账号的泄露。所以这里建议所有机场都不应该使用常用的邮箱(如xxx@qq.com)进行注册。(卖Gmail!5块一个

    明确底线。虽然所谓的各种协议可以保护你和服务器之间的通信,确保信息被加密不被审查,但对于机场运营来说,你的所有浏览信息都是清晰可见的,如果TA想的话,甚至可以查到你昨晚在几点钟看了哪几部动作电影。部分机场还有有可能保存日志(Log)。所以机场和VPN并不代表着绝对隐私和安全,法律的底线在这里也并没有失效,请在快乐冲浪时明确这条底线。

    不同的机场使用不同邮箱。这是一条进阶建议。不同机场使用不同的邮箱不但可以很好的隔离个人信息,同时还会带来诸多便利。比如日后如果想更换机场,但目前机场还有大量未过期的套餐,可以选择连带邮箱一起进行出售,同时不用担心会对自己其他的信息造成泄露等等。

    文章推荐

    「杂谈」关于Telegram Premium的一些事

  • 如何在国外网站上氪金——中行 跨境通卡 介绍

    笔者的中行跨境通非人哉白泽卡

    更新速览 22.1.26

    非人哉莫奈卡可网申上海分行版。

    冬奥主题借记卡推出雪板异形卡。

    Mora、DLsite、Pixiv 系(Booth、Fanbox 等)能够正常用卡。

    根据水友 胖次 的留言提醒,Mora的乐天pay支付方式疑似404。

    根据Selene Elsevier的反馈,修正了部分卡面图片。

    补充了Visa御玺卡/Master世界卡的办理条件。

    补充了网申的办理流程。

    从2022.2.1日起,跨境通卡不收取年费(包括此前欠缴的年费)。


    好久不见,我是安咕咕。今天我为大家介绍的是中行 跨境通卡 。

    不知道大家有没有这样的困扰,平时自己想买一些国外周边,或者是给国外Google Play上的游戏氪金时,没有合适的付款方式,Paypal不可用,买点卡又怕被奸商讹诈……那么,接下来这个教程可能帮到你。

    前排提示:请读者不要使用该卡进行各种违法犯罪活动,不要轻信各种黑产广告,打击电信诈骗和黑产人人有责!

    还有,不要用我的截图去做坏事(虽然我应该是在关键个人信息部位都打了码)

    全文阅读大约用时14分钟。

    本文写作时为图方便,直接采用默认省市区的行政区划进行表达,若你位于直辖市、州、自治区的等特殊行政区划,还请自行匹配。

    转载本文前请先联系笔者,未经许可禁止转载。

    简介

    长城跨境通国际借记卡是中国银行与国际上最大的两家银行卡组织(万事达、VISA)合作,在国内推出的首张搭载EMV芯片标准的国际多币种借记卡,是具有接触/非接触功能的磁条芯片复合卡。该卡专为商旅出境以及留学人员量身定制,除了创造更为安全便捷的支付体验之外,还提供“近20种交易币种无货币转换直接扣账”、“免货币转换手续费”和“境外ATM取现手续费减免”等多项创新和优惠,方便客户在境外取现和消费,让您走遍全球、畅行无忧。

    以上为官方介绍。

    优点

    真正的借记卡,不上信报

    这张卡并非某些银行以借记卡名义发行的零额度信用卡,是真真正正的借记卡。你这张卡的消费记录等信息是不会上传到人行信用中心的,不会对你的贷款造成任何影响。

    多币种支持,不收手续费与年费

    从简介就能看出,这张卡提供“近20种交易币种无货币转换直接扣账”、“免货币转换手续费”和“境外ATM取现手续费减免”等多项创新和优惠。而且中行的汇率价格会比其他银行稍微优惠一些。

    手续简单,几乎无条件限制

    这张卡只需要身份证件即可办理,无需其他繁琐的证明材料。(据说好像可以网申?但笔者没实践过)

    支持3D验证服务

    这个……既是优点也是缺点了,一会儿专门细说。

    EMV 芯片卡,支持非接触功能

    国内首张搭载 EMV 芯片标准的国际借记卡,属于磁条芯片复合卡。

    支持Visa payWave和MasterCard Contactless非接触式支付功能。

    卡权益和银行优惠活动

    可以享受卡组织提供的卡权益和参与银行举办的各种优惠活动。

    种类

    卡片名称卡组织备注
    标准 金卡Visa/Master
    标准 白金卡Visa/Master
    非人哉小玉/白泽卡 金卡Master上海分行网申链接
    冬奥 蓝/黑卡 御玺卡VISA(有异形款 异形款收取额外工本费 RMB¥ 50)
    莫奈卡 世界卡Master上海分行网申链接
    选校帝卡 御玺卡Visa
    公派留学生专用卡暂不介绍(我真没渠道能办下来那玩意……)

    开卡条件

    年满十六周岁

    根据中行相关规定,未满十六岁周岁的未成年人需要在监护人陪同下办理开户开卡业务。

    部分地区的柜员可能会阻止未成年人的办卡,或要求出具监护人或本人的相关材料,详询当地网点柜员。

    持有一张中行借记卡

    此卡需满足:

    • 本人名下
    • 一类卡/一类账户
    • 最好开通并绑定中行手机银行

    中行借记卡数量少于4张

    根据中行规定,同一人在中行开立实体借记卡原则上不得超过4张。

    注:御玺卡、世界卡有额外开通条件:(二者满足其一)

    • 中行流动资产三十万元人民币
    • 持有中行或其他商业银行同等级或更高等级卡片

    开卡流程

    线下申请

    优点

    • 速度快
    • 审核门槛较低

    缺点

    • 有可能缺卡板:由于跨境通卡是相对冷门的卡种,许多网点(特别是位于小城市、城镇的小网点)是不提供跨境通卡卡板的。即使是省分行、市分行级别的网点,也往往只提供数量有限的卡板。(我的非人哉就是全市剩下的唯一一张卡板)
    • (小概率情况下)柜员可能操作错误导致此卡不可用。

    网点选择

    笔者建议在选择网点时,优先选择省分行、市分行等大型网点进行办理,这里的柜员态度亲和、业务熟练、几乎不会发生开卡操作出错的问题。同时,大网点一般卡板存量多(尤其是部分稀少卡板),即使当时没有卡板,它们也是最快补充新卡板的网点。

    开卡准备

    • 身份证件
    • 你名下的中行一类卡(没有可以现场开卡,也很方便)
    • 装有本人名下手机卡的智能手机(方便预留手机号和开通网上银行功能)
    • 遵守当地防疫政策要求

    开卡

    进入银行后,请明确告知大堂经理(站在入口附近协助客户办理业务的员工)你要直接排号去柜台办理中行长城跨境通借记卡(因为大堂经理一般不太了解这种卡,他们可能会给你推荐Visa/Master的信用卡并让你在自助柜员机办理)。

    来到柜台时,请明确告知柜员(隔在玻璃窗后面办理业务的员工)你要办理中行长城跨境通国际借记卡,他们可能不太熟悉这个名称,你可以使用“VISA/MasterCard借记卡”、“EMV借记卡”这两个名称,或者直接将你想办理的卡面图片展示给他们看(我会在文末展示各种卡面的图片),他们会去后台为你寻找卡板。

    如果很幸运,他们找到了 跨境通卡 的卡板,那么你就可以正常办理啦。

    办理时可能会要求你签署公安部、公安局、银行的各种协议与文件。部分地区网点可能会收取 5 元工本费。

    办理后银行可能会定期电话回访,询问你银行卡的状况,并向你转达公安部的安全提示。

    办理后柜员可能会向你推荐优惠活动,笔者建议你不要拒绝,上次笔者就参加了刷卡95折返现活动,很划算。

    线上申请(上海分行公众号)

    优点

    • 申卡流程十分完善成熟(如提供邮寄服务、进度查询、自助激活销卡等)
    • 开户行是上海市分行。可以参加上海市分行的各种活动。(它们活动蛮多的其实)

    缺点

    • 开户行是上海市分行。办理补卡、换卡业务需要去上海分行线下办理。(补卡是一定要去那边的、换卡暂不确定)

    由于我本人并没有网申记录,所以我这里转载了poplite大佬的方法,仅供参考。

    开卡链接

    目前只有非人哉和莫奈卡

    非人哉:

    https://cloud.bankofchina.com/sh/api/net/common/url/adr?id=kuajinggofeirenzaicard

    莫奈:

    https://cloud.bankofchina.com/sh/api/net/common/url/adr?id=kuajinggomastercard

    开卡(以莫奈卡为例)

    申请页面
    网上填写资料

    按照网申链接上的指引操作即可。需要提交的资料有身份证正反面、头像照、个人信息、邮寄地址(精确到门牌号)。

    进度查询

    从申请页面点击左上角退出,右上角便有“申请进度”按钮查询。

    右上角(标题链接与右上角链接相同)

    进度查询如果“当前状态”变为“制卡中”,说明申请成功。

    申请成功
    激活卡片

    一般过两到四周后,可以收到上海分行邮寄的卡片挂号信。(目前受疫情影响,至少要一个月才能收到此挂号信)

    挂号信

    按照挂号信的指示,前往全国任意中行网点,在智能柜台上自助激活即可。

    注意事项
    • “快递单号”存在 BUG。经常出现收到卡后才会显示快递单号的情况。
    • 如果提示“审核未通过”,可能是没有填写详细的邮寄地址。(邮寄地址一定要写到门牌号)
    • 如果提示“开卡失败”,可能是上海分行系统出问题。需要重新申请。
    • 根据Selene Elsevier大佬的提示,线下激活的时候假如柜员啥也不懂,可以自己去智能柜台机操作激活 按借记卡激活,初始密码是身份证去掉最后一位后取后六位。不过,借记卡激活需要大堂经理审批,所以最后还得被大堂经理审视一番。

    购汇与交易

    购汇

    如果你仔细观察这张卡,你会发现,它没有银联标识,这也就意味着,它不能直接存人民币。所以,我们要进行购汇,将外币存入到这张卡上。

    外汇小知识

    首先,请允许我为各位科普一些外汇小知识。

    我国外汇管理局规定,个人每年的购汇年度总额为等值5万美元,如超过年度总额购汇,银行将按照外汇管理规定,线下审核你的真实需求凭证后,为你办理购汇业务。

    也就是说,你超额度也没关系,只是需要你去线下进行购汇。(不会吧不会吧不会真的有大佬一年要氪30多万吧……)

    注:如果需要超额度购汇,可参照下方表格准备相关资料

    用途需要提供的真实凭证
    自费出境学习学费、生活费本人因私护照及有效签证(或签注);境外学校录取通知书(购买第二学年或学期以后的学费或生活费无需提供);境外学校相应年度或学期学费证明或生活费用证明
    境外就医 本人因私护照及有效签证(或签注);境内医院出具的证明、附医生意见;境外医院出具的费用证明
    境外培训 本人因私护照及有效签证(或签注);境外培训费用证明
    缴纳境外国际组织会费境外国际组织缴费通知
    境外直系亲属救助有关部门或公证机构出具的亲属关系证明;有关救助的相关证明材料
    境外邮购 广告或定单等收费凭证、网上下载件
    境外咨询合同(协议)、发票(支付通知)、税务凭证等
    中国银行超额购汇所需的真实凭证

    其次,境内个人外汇账户只能向本人同名账户或直系亲属账户转账。而且向直系亲属账户转账极为不便(线下+证明材料),所以没有特殊需求,尽量使用本人名下的中行银联卡购汇,以避免不必要的麻烦。

    此外,结售汇时会涉及以下四个名词:银行买入价、银行卖出价、现汇、现钞

    2022年1月17日 12:21:50 日元外汇牌价

    可以看到,图中分别有 现汇的买入卖出价现钞的买入卖出价 ,意味着您可以选择购入现汇或者现钞,您可以参考以下表格:

    种类现汇现钞
    用途银行转账和购买银行转账和购买,柜台/自助ATM取款(外币)
    价格一般情况下低于现钞价格一般情况下高于现汇价格

    如上表,现汇和现钞的主要区别是用途不同。现汇只能用于银行转账和购买,而现钞除上述功能外还可以用来在ATM或柜台取款(是真正的实体货币哦,虽然没有正当理由银行一般不会让你取就是了),所以,个人购买现钞的价格一般比现汇贵了那么一点(有时还不止一点……),如果你是正常氪金网购的话,买现汇就可以了。

    中行外汇价格表达方式:他国 每100单位货币 兑换 等值人民币 。以图中银行中间价为例,它表示每100日元兑换人民币5.578元。

    中间价:买入价和卖出价的平均值,即:

    中间价=(现汇买入价+现汇卖出价)/2,

    中间价一般做参考价使用。

    银行买入/卖出价:以银行为主体,向顾客买入/卖出外汇的价格。

    举个例子,如果你现在需要买入100日元的现汇,则相当于以银行为主体向你卖出100日元的现汇,所以价格按照银行现汇卖出价进行计算。

    由于现汇和现钞的银行的买入价总是低于卖出价,所以即时买入再卖出外汇一定亏钱,所以所以一定要用多少买多少,不然差价都被银行赚走了(当然如果您是土豪就无所谓了

    啊对了,如果你愿意稍微用点心的话,可以关注一下汇率走势,挑价格便宜的时候买。

    购汇方法

    以下为手机银行购汇125日元演示(别问为什么只买125,问就是没有钱)

    中国银行目前只在北京时间8:00——22:00开放相关业务。

    看一下手机银行是否关联了你的一类卡和 跨境通卡 (如果没有去就近网点办一下就好)。

    打开手机银行,登录后点击“结汇购汇”

    点击“购汇”

    30秒后点击下方同意。

    币种支持很多
    购汇用途(选项很多这里不另作展示,按需求填就可以,氪金可以填海外购物)

    填好后点下一步。

    填完大概是这样

    最后点确认。

    弹出这个页面,购汇就OK了。

    转账

    购汇之后,就开始向新办的卡转账了。

    如果熟悉了应用界面,可以在主页设置快捷转账,一键直达,这里就留给大家慢慢探索了。

    回到首页后,点击账户管理。

    点你刚才购汇用的卡的转账

    (富婆,饿饿,饭饭)
    悄悄晒卡(怎么拍这么丑……实际比照片好看多了)

    切换币种(我这里切换成日元)

    点全部转出,再点下方的收款人

    点自己办理的那张 跨境通卡 ,然后点击下一步

    点确认

    出现这个页面就转账成功啦。

    PS:一部分人在网络上反馈,中行校园卡即使是一类卡也不能进行线上购汇。由于我的校园卡是二类卡,所以无法验证这一消息的真伪,若出现了相关问题,可以向银行柜员咨询解决方案。

    绑卡

    poplite大佬总结说:

    经过长时间检验,可以确定跨境通卡的支付功能基本与国际信用卡相同,即:

    除了少数网站,只要境外网站能够使用国内发行的外标信用卡支付,跨境通卡同样也能在该网站上使用。

    同理,如果网站不支持(或禁用)国内信用卡,跨境通卡一般也无法使用。

    根据poplite大佬的整理与总结和我的个人体验,以下列出此卡支持和不支持的网站。

    绝大部分数据最后更新时间为2020年2月18日,所以以下表格仅供参考,请以实际使用为准,欢迎在评论区讨论。

    网站测试区域备注
    Google Play
    (直接绑卡+PayPal)
    美区 / 日区日区绑卡预授权204日元
    (笔者测试时貌似不需要)
    若出现无法绑卡/付款失败,
    可联系客服解封卡片。
    PayPal美区 / 国区 / 港区
    Spotify
    (PayPal)
    美区
    亚马逊日区
    Netflix美区
    Nintendo eShop日区
    支持网站1
    网站备注
    DMM
    (直接绑卡)
    只能充值点数
    虎之穴
    Melonbooks
    Pixiv
    (直接绑卡+PayPal)
    Pixiv Fanbox
    (直接绑卡+PayPal)
    BOOTH.pm
    (直接绑卡+PayPal)
    Mora.jp
    (直接绑卡+Amazon Pay
    +乐天 Pay)
    Poplite大佬的
    Amazon Pay教程
    乐天 Pay教程
    (乐天Pay疑似404)
    ConoHa
    DLsitePS:我个人是不太推荐在魔法集市买点数的
    它们的手续费+汇率算下来贵了差了不少
    支持网站2
    网站区域备注
    Google Pay美区不支持国内卡
    (有待查证)
    Google Pay Send
    (原 Google Wallet)
    美区识别为信用卡
    (有待查证)
    App Store
    (直接绑卡)
    所有区域锁区
    (可用对应区Paypal尝试)
    Spotify
    (直接绑卡)
    所有区域锁区
    不支持网站1
    网站备注
    支付宝、微信
    (国内版)
    禁止绑定境内外标卡
    (国际版似乎可用)
    不支持网站2

    下图为笔者在Google Play和DLsite上的付款记录,仅供参考。

    Google Play

    Google Play绑卡界面
    我的部分支付记录(很长时间没玩了)
    Google Play 账单邮件
    银行流水

    DLsite

    DLsite 绑卡界面
    购买记录(NSFW)
    银行流水

    注意事项

    交易与预付款

    和其他的外标信用卡相同, 跨境通卡 采用先预授权、后入账扣款的交易方式。即消费时不会立即扣款,而是先预授权(冻结)特定比例的资金。如果用官方的话来说就是:

    申请人通过VISA、MasterCard 等国际银行卡组织网络进行的交易属双信息交易,即卡片发生交易时,先在持卡人的账户中冻结交易金额,当国际银行卡组织与发卡银行进行结算时再从持卡人账户中完成扣款。

    中国银行股份有限公司长城借记卡章程(2021年版)

    入账时间一般是两至三天。

    而这就产生了以下两个问题:

    • 手机银行无法查看未入账交易
      • 手机银行只能显示所有未入账交易总和的预授权金额,既不能单独显示每一笔未入账交易,也不能查询商户名、消费金额和消费时间等交易信息。
      • 举个例子: 如果你在某天连续刷了 $100、$200 和 $300 三笔交易,当天手机银行只能显示美元账户存在 $600 的预授权金额,除此之外无更多信息。
      • 解决方式:去银行柜台查询。
    • 微信交易提醒存在延迟
      • 中国银行微信服务号只推送入账交易通知、不推送预授权交易通知。即发生消费(预授权)时不发送通知,两至三天后入账时才发送通知,存在时间延迟。
      • 解决方式:办理收费的短信交易提醒(2元/月)。

    交易与余额

    一些网站在首次付款或者绑卡后长时间没有发生付款的情况下,为验证卡片状态是否正常,网站一般先预授权(冻结)一笔小额资金(大约为1美元或1单位网站收款方所在当地货币,有些网站可能会更多),过一段时间后退还,所以建议各位第一次付款时卡内多准备一些货币。

    此外,在实际使用中,如果使用非美元进行结算,银行会预授权(冻结)实际支付金额的102%,否则支付失败。多出的2%待入账后退还(美元预授权为100%)。所以转账时一定要算好交易金额。

    交易与扣款币种

    跨境通卡 支持近20种外币。每种外币分为外汇和外钞,故最多可以拥有近40个外币账户。

    而跨境通卡外币账户的扣款顺序为:交易币种钞户 > 交易币种汇户 > 美元钞户 > 美元汇户(没有人民币)如果用官方的话来说就是:

    VISA、MasterCard 多币种卡账户如有原始交易货币的存款且余额充足,直接在该账户扣款;如交易地币种不在以上外币范围内,或对应外币存款账户无余额或余额不足时,还可按照VISA、MasterCard或发卡银行折算(如原始交易货币为VISA、MasterCard清算货币)的美元交易金额使用美元账户扣款;如无美元账户或美元账户余额不足时,则交易失败。

    中国银行股份有限公司长城借记卡章程(2021年版)

    所以:

    • 非美元的外币账户只能支付对应币种(如英镑账户只能支付英镑,不能支付美元、港元等其他币种。)
    • 跨境通卡支持外币以外的币种只能使用美元账户支付(如跨境通卡不支持新台币直接支付。即使跨境通卡存有新台币外币,也只能从美元账户扣款。)
    • 当美元账户支付非美元,使用VISA/MasterCard卡组织汇率结算(Visa/Master赚你差价)
    • 不支持多账户支付,即只能使用一个外币账户全额支付,不能从多个账户扣款。
    • 同币种钞户优先与汇户

    3D验证服务

    3D验证服务(Verified by Visa、MasterCard SecureCode) 是VISA/MasterCard卡组织验证持卡人的方式,常见于日本、香港等亚洲地区网站。 跨境通卡 支持3D验证服务,若网站开启3D验证,付款前需要跳转到中行的3D验证网页,输入银行预留手机号和短信验证码,验证持卡人身份。

    3D验证服务(转自poplite.xyz

    同样,这里也有几个问题:

    • 只支持短信验证。如果手机信号不好或者家住境外,可能不方便接受到验证码短信。(明明中行信用卡就支持微信动态口令认证)
    • 若3D验证连续失败次数过多,3D验证锁定且无法解锁(此部分有待考证)

    跨境通卡 与账单地址

    跨境通卡不检验帐单地址。 跨境通卡与国内外标信用卡一样,不支持AVS(地址验证服务)验证。

    其他常见误区

    以下内容大多转自poplite.xyz,若评论区有疑问我也会补充进来。

    跨境通卡是零额度信用卡✘

    跨境通卡是真正的VISA / MasterCard国际借记卡。除了卡组织不同, 跨境通卡与中行发行的银联借记卡本质上是一样的。

    跨境通卡是单币种卡 / 是美元卡 / 只有美元账户✘

    跨境通卡是多币种卡,支持美元、欧元、日元、港币等 19 种外币。

    跨境通卡消费冻结 105% / 120% / 150%✘

    跨境通卡美元消费冻结 100%、非美元消费冻结 102%。冻结比例与交易币种有关。

    跨境通卡有年费 / 年费是 10 美金 / 20 美金 / 50 美金✘

    跨境通卡不收取任何年费

    跨境通卡消费存在手续费✘

    除 ATM 取现费之外,跨境通卡消费不收取任何额外费用(例如货币转换费、跨境手续费等)。非美元消费额外冻结的 2% 资金不是手续费,入账后将返还至原卡。

    跨境通卡是国内唯一一种可以申请到的外标借记卡✘

    根据poplite.xyz的描述,中信银行于2019年7月新发行了一款MasterCard外标借记卡。此外,北京银行、工商银行等银行都有发行外标借记卡(申请难度、用卡体验不同)。

    优惠相关

    跨境通卡 可以参加Visa/Master的优惠活动,关注相关微信公众号即可参与。

    中行也会有相关的优惠活动,详询柜台工作人员获取最新活动咨询。

    总体来说,万事达的优惠比Visa多一些。

    卡面图片

    万事达标准金卡
    Visa标准金卡
    万事达标准白金卡
    Visa标准白金卡
    非人哉小玉卡/白泽卡 万事达金卡
    冬奥蓝/黑卡 Visa御玺卡
    选校帝卡 Visa御玺卡
    莫奈卡 万事达世界卡

    引用与鸣谢

    这篇教程主要参考与借鉴了poplite大佬的文章,向我们的引领者和探索者致敬。

    [2021年更新] 跨境通VISA/万事达借记卡介绍与网上支付体验 https://poplite.xyz/post/2018/03/05/boc-debit-card-guide-for-online-payment.html poplite

    感谢nf对本次排版的优化(我排版排的直接裂开……)

    为了seo优化,可能有些部分文本看起来会有点奇怪,还请见谅。

    作者相关

    安咕咕,是一名普普通通、平平无奇、随处可见的文科生。希望自己的文章能给其他人带来帮助。

    如果您喜欢这篇文章,不妨将其转发给其他人。

    欢迎加入笔者的Telegram频道

    本文章为作者原创,请确认征得原作者同意后进行转载。