zoukankan      html  css  js  c++  java
  • UE4多线程概述

    为了提升游戏的运行帧率,减少卡顿,UE4中使用了大量的线程来提升游戏的并发程度,来释放GamePlay游戏线程的压力。

    具体包括:

    ① 将渲染的应用程序阶段的工作放在RenderThread中

    ② 将渲染命令提交放在RHIThread中

    ③ 将Actor及ActorComponent的Tick、GC Mark等放到TaskGraph中并行化

    ④ GC Sweep的内存释放逻辑放在FAsyncPurge线程中

    ⑤ 资源放到AsyncLoading线程中异步加载

    ⑥ 物理模拟会在PhysX内部的线程中计算

    ⑦ 声音放在AudioThread线程中

    ⑧ Stat统计数据的收集放到StatThread线程中

    ⑨ 在FAsyncWriter线程中写log到文件

    #include "CoreMinimal.h"
    
    DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All);
    
    IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMultiThreadTest, "MyTest1.PublicTest.MultiThreadTest", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)  // 可被Automation System识别
    
    
    class FTest1Runable : public FRunnable
    {
    public:
        FTest1Runable(int32 index)
        {
            ThreadIndex = index;
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Contruct: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        }
    
        virtual ~FTest1Runable() override
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Destruct: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        }
    
        virtual bool Init() override   // 线程创建后,执行初始化工作   【在线程上执行】
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Init: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            return true;
        }
        virtual uint32 Run() override   // 放置线程运行的代码   【在线程上执行】
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 111 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            
            // FPlatformProcess::Sleep(xx)可让当前线程Suspend xx秒   注:其他线程不受影响
            switch (ThreadIndex)
            {
            case 0:
                FPlatformProcess::Sleep(20.0f);
            default:
                FPlatformProcess::Sleep(1.0f);
                break;
            }
            
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 222 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            FPlatformProcess::Sleep(2.0f);
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 333 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            return 0;
        }
        virtual void Stop() override
        {
        }
    
        virtual void Exit() override   // 线程退出之前,进行清理工作   【在线程上执行】
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Exit: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        }
    
    private:
        int32 ThreadIndex;
    };
    
    
    bool FMultiThreadTest::RunTest(const FString& Parameters)
    {
        UE_LOG(TestLog, Log, TEXT("RunTest Begin tid:0x%x FrameIndex:%llu"), FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
    
        // 调用FRunnableThread::Create来创建一个线程
        FRunnableThread* Test1Thread0 = FRunnableThread::Create(new FTest1Runable(0), TEXT("Test1Thread0"));  // Test1Thread0为线程名
        FRunnableThread* Test1Thread1 = FRunnableThread::Create(new FTest1Runable(1), TEXT("Test1Thread1"));
        FRunnableThread* Test1Thread2 = FRunnableThread::Create(new FTest1Runable(2), TEXT("Test1Thread2"));
    
        UE_LOG(TestLog, Log, TEXT("RunTest End tid:0x%x FrameIndex:%llu"), FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        return true;
    }

    Automation System(自动化测试系统)  菜单:Window -- Developer Tools -- Session Frontend

     

    执行流程解释如下:

    // 游戏线程 ID:0xe08     Test1Thread0 ID: 0xa67c     Test1Thread1 ID: 0x7104     Test1Thread2 ID: 0xb294
    
    // 在第6390帧  游戏线程调用FRunnableThread::Create创造出Test1Thread0、Test1Thread1、Test1Thread2
    //            各个线程被创建出来后,在自己的线程中立即调用了Init和Run方法
    [2020.09.28-07.11.59:411][390]TestLog: RunTest Begin tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:411][390]TestLog: FTest1Runable Contruct: ThreadIndex:0 tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Init: ThreadIndex:0 tid:0xa67c FrameIndex:6390
    [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Run: 111 ThreadIndex:0 tid:0xa67c FrameIndex:6390
    [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Contruct: ThreadIndex:1 tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Init: ThreadIndex:1 tid:0x7104 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Run: 111 ThreadIndex:1 tid:0x7104 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Contruct: ThreadIndex:2 tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Init: ThreadIndex:2 tid:0xb294 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Run: 111 ThreadIndex:2 tid:0xb294 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: RunTest End tid:0xe08 FrameIndex:6390
    
    // 在第6474帧
    // 过了1s,线程Test1Thread1、Test1Thread2执行到Run 222处
    [2020.09.28-07.12.00:418][475]TestLog: FTest1Runable Run: 222 ThreadIndex:1 tid:0x7104 FrameIndex:6474
    [2020.09.28-07.12.00:418][475]TestLog: FTest1Runable Run: 222 ThreadIndex:2 tid:0xb294 FrameIndex:6474
    
    
    // 在第6690帧
    // 再过了2s,线程Test1Thread1、Test1Thread2退出Run函数,并调用Exit函数,线程的生命周期结束
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Run: 333 ThreadIndex:2 tid:0xb294 FrameIndex:6690
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Exit: ThreadIndex:2 tid:0xb294 FrameIndex:6690
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Run: 333 ThreadIndex:1 tid:0x7104 FrameIndex:6690
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Exit: ThreadIndex:1 tid:0x7104 FrameIndex:6690
    
    // 在第8832帧
    // 过了10s,线程Test1Thread0执行到Run 222处
    [2020.09.28-07.12.20:419][833]TestLog: FTest1Runable Run: 222 ThreadIndex:0 tid:0xa67c FrameIndex:8832
    
    // 在第9072帧
    // 再过了2s,线程Test1Thread0退出Run函数,并调用Exit函数,线程的生命周期结束
    [2020.09.28-07.12.22:419][ 73]TestLog: FTest1Runable Run: 333 ThreadIndex:0 tid:0xa67c FrameIndex:9072
    [2020.09.28-07.12.22:419][ 73]TestLog: FTest1Runable Exit: ThreadIndex:0 tid:0xa67c FrameIndex:9072

    如果想在单线程运行模式下(带上-nothreading参数),以假线程FFakeThread的方式在游戏线程上来模拟执行的话,需要从FSingleThreadRunnable派生,并实现其GetSingleThreadInterface接口中返回当前this指针

    class FStatsThread
        : public FRunnable
        , private FSingleThreadRunnable
    {
    // ... ...
    public:
        virtual FSingleThreadRunnable* GetSingleThreadInterface() override
        {
            return this;
        }
    };

    1. FRunnable是线程可执行实体,其Run()函数为线程函数。Run()函数执行完,线程的生命周期也将结束

    2. FRunnableThread是所有线程的基类,FRunnable为其成员变量

    3. FRunnableThread相当于一个“外壳”,根据平台会创建出属于那个平台的线程。而FRunnable是“核”,定义了这个线程具体要做什么

    4. FThreadManager是全局的线程管理单例(通过静态函数FThreadManager::Get()得到单例),可获取到当前运行的所有线程

    5. FRunnableThreadWin用于windows平台,FRunnableThreadPThread(对pthread的封装)用于Android、iOS、Mac、Linux等平台

    6. FFakeThread用于单线程运行模式下(带上-onethread参数),以假线程的方式在游戏线程上来模拟执行

    7. FRunnableThread相关函数

       const uint32 GetThreadID() const; // 线程ID,唯一

       const FString & GetThreadName() const; // 线程名称 可重复

       EThreadPriority GetThreadPriority();  // 获取线程的优先级

       void SetThreadPriority( EThreadPriority NewPriority );  // 设置线程的优先级

       void Suspend( bool bShouldPause = true );   // bShouldPause为true时,pause线程;bShouldPause为false时,resume线程

       void WaitForCompletion();  // 阻塞并等待当前线程执行完毕

       bool Kill( bool bShouldWait = true );   // 会先执行 runnable 对象的Stop函数,然后根据 bShouldWait 参数决定是否等待线程执行完毕。如果不等待,则强制杀死线程,可能会造成内存泄漏

    线程 平台 解释

    主线程

     All

    windows:为游戏线程

    线程函数为:LaunchWindows.cpp下的WinMain函数

    Android:线程名为MainThread-UE4

    使用java代码来处理主消息循环

    Splash Activity:EngineBuildAndroidJavasrccomepicgamesue4SplashActivity.java

    游戏Activity:EngineBuildAndroidJavasrccomepicgamesue4GameActivity.java.template

    iOS:线程名为Thread <n>  如:Thead 1

    线程函数为LaunchIOS.cpp下的main函数

    游戏线程(消耗高)

     All

    Windows:线程名为Main Thread

    线程函数为:LaunchWindows.cpp下的WinMain函数

    Android:线程名为Thread-<n>  如:Thread-3

    线程函数为LaunchAndroid.cpp下的android_main函数

    iOS:线程名为Thread <n>  如:Thread 5

    线程函数为IOSAppDelegate.cpp下的MainAppThread函数

    线程id:uint32 GGameThreadId

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread)

    bool IsInGameThread();

    渲染线程(消耗高)

    class FRenderingThread : public FRunnable

    线程函数:RenderingThreadMain

     All

    线程名:RenderThread 1

    -norenderthread参数可在启动时强制不开启RenderThread

    注:开启-norenderthread参数时,最好是带上-NoLoadingScreen来禁掉loadingscreen,要不然会出现崩溃

    可用ToggleRenderingThread控制台命令来创建和销毁RenderThread

    销毁RenderThread时,渲染相关的逻辑会放回到游戏线程中

    渲染线程的创建逻辑在void StartRenderingThread()函数中

    销毁逻辑在void StopRenderingThread()函数中

    线程id:uint32 GRenderThreadId

    FRunnableThread* GRenderingThread;

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(RenderThread)

    bool GUseThreadedRendering; // 是否使用独立线程来渲染

    bool GIsThreadedRendering;  // 渲染是否在独立的线程中运行

    TAtomic<int32> GIsRenderingThreadSuspended;  // 渲染线程是否暂停

    TAtomic<int32> GSuspendRenderingTickables;  // rendering tickables是否应该更新   flush时应暂停更新

    bool IsInActualRenderingThread();  // 渲染线程存在,且当前线程为渲染线程

    bool IsInRenderingThread()

    bool IsInParallelRenderingThread();

    RHI线程

    class FRHIThread : public FRunnable

     All

    使用独立的渲染线程时,会根据情况来决定是否创建该线程

    当渲染线程销毁时,该线程也会销毁,RHI执行的逻辑会放回游戏线程中

    -rhithread参数时(缺省),由各个平台来决定启动时是否开启RHI线程

    对应bool GRHISupportsRHIThread变量,具体情况如下:

    DX11缺省不开启,可以通过#define EXPERIMENTAL_D3D11_RHITHREAD 1来缺省开启

    DX12在非Editor模式下默认开启

    OpenGL根据FeatureLevel和r.OpenGL.AllowRHIThread的值来决定是否缺省开启
    r.OpenGL.AllowRHIThread 0 // 不使用RHI线程
    r.OpenGL.AllowRHIThread 1 // 使用RHI线程(缺省)

    Vulklan缺省开启1个RHI线程
    r.Vulkan.RHIThread 0 // 不使用RHI线程
    r.Vulkan.RHIThread 1 // 使用1个RHI线程(缺省)
    r.Vulkan.RHIThread 2 // 使用多个RHI线程

    Metal会根据显卡芯片版本和r.Metal.IOSRHIThread的值来决定是否缺省开启
    r.Metal.IOSRHIThread 0 // 不使用RHI线程(缺省)
    r.Metal.IOSRHIThread 1 // 使用1个RHI线程

    -norhithread参数会在启动时强制不开启rhi线程

    如果不开启rhi线程,rhi的逻辑会跑在RenderingThread线程中

    在pc环境的运行时,可用r.RHIThread.Enable 0控制台命令切到这种方式来跑

    线程id:uint32 GRHIThreadId

    FRunnableThread* GRHIThread_InternalUseOnly

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::RHIThread)

    // 创建独立的RHIThread,放加入到TaskGraph中,RHI会跑在TaskGraph的RHIThread上

    // 在pc环境的运行时,可用r.RHIThread.Enable 1控制台命令切到这种方式来跑

    bool GUseRHIThread_InternalUseOnly;  

    // 在pc环境的运行时,可用r.RHIThread.Enable 2控制台命令切到这种方式来跑

    bool GUseRHITaskThreads_InternalUseOnly;  // TaskGraph中使用Any Thread来跑

    // GUseRHIThread_InternalUseOnly为true,且正在运行时,该值为true

    bool GIsRunningRHIInDedicatedThread_InternalUseOnly;  --》bool IsRunningRHIInDedicatedThread()

    // GUseRHITaskThreads_InternalUseOnly为true,且正在运行时,该值为true

    bool GIsRunningRHIInTaskThread_InternalUseOnly; --》bool IsRunningRHIInTaskThread()

    // GUseRHIThread_InternalUseOnly或GUseRHITaskThreads_InternalUseOnly为true,且正在运行时,该值为true

    bool GIsRunningRHIInSeparateThread_InternalUseOnly; --》bool IsRunningRHIInSeparateThread()

    bool IsInRHIThread();

    RenderingThread心跳监视线程

    class FRenderingThreadTickHeartbeat : public FRunnable

    All

    线程名:RTHeartBeat 1

    负责执行rendering thread tickables

    使用独立的渲染线程时,才会创建该线程,来执行void TickRenderingTickables()

    当渲染线程销毁时,该线程也会销毁,TickRenderingTickables会放回游戏线程中来执行

    TAtomic<bool> GRunRenderingThreadHeartbeat  // 是否使用RenderingThread心跳监视线程

                                                                           // 当使用独立线程来渲染时,会被设置成true;独立渲染线程停止时,会被设置成false

    float GRenderingThreadMaxIdleTickFrequency = 40.f;  // tick频率  值越大tick次数越多,性能越差

    线程池线程

    class FQueuedThread : public FRunnable

    All

    线程名:PoolThread 0  ...  PoolThread <n>

    // Global thread pool for shared async operations
    extern CORE_API FQueuedThreadPool* GThreadPool;
    extern CORE_API FQueuedThreadPool* GIOThreadPool;
    extern CORE_API FQueuedThreadPool* GBackgroundPriorityThreadPool;

    #if WITH_EDITOR
    extern CORE_API FQueuedThreadPool* GLargeThreadPool;
    #endif

    TaskGraph线程(消耗高)

    class FTaskThreadAnyThread : public FTaskThreadBase

    注:class FTaskThreadBase : public FRunnable, FSingleThreadRunnable

     All

    线程名:TaskGraphThreadHP 0  ...  TaskGraphThreadHP <n>

               TaskGraphThreadNP 0  ...  TaskGraphThreadNP <n>

               TaskGraphThreadBP 0  ...  TaskGraphThreadBP <n>

    进程优先级:HP > NP > BP

    TaskGraph只在最开始构造函数中创建所有的线程,调用栈如下:

    UE4Editor-Core-Win64-Debug.dll!FTaskGraphImplementation::FTaskGraphImplementation(int __formal) Line 1158
    UE4Editor-Core-Win64-Debug.dll!FTaskGraphInterface::Startup(int NumThreads) Line 1692
    UE4Editor-Win64-Debug.exe!FEngineLoop::PreInitPreStartupScreen(const wchar_t * CmdLine) Line 2052
    UE4Editor-Win64-Debug.exe!FEngineLoop::PreInit(const wchar_t * CmdLine) Line 3606
    UE4Editor-Win64-Debug.exe!EnginePreInit(const wchar_t * CmdLine)
    UE4Editor-Win64-Debug.exe!GuardedMain(const wchar_t * CmdLine)
    UE4Editor-Win64-Debug.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow) Line 257

    AudioThread线程(消耗较高)

    class FAudioThread : public FRunnable

    线程函数:AudioThreadMain

     All

    线程名:AudioThread

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::AudioThread)

    bool IsInAudioThread();

    AudioMixerRenderThread线程(消耗高)

    class IAudioMixerPlatformInterface : public FRunnable, public FSingleThreadRunnable, public IAudioMixerDeviceChangedLister

     All 线程名:AudioMixerRenderThread

    class FAsyncLoadingThread final : public FRunnable, public IAsyncPackageLoader

     All

    在EDL(EventDrivenLoader)模式下,是不开启该独立线程的,在主线程中Loading

    bool GEventDrivenLoaderEnabled为ture时为EDL模式

    编辑器、standalone非cook版本为EDL模式,没有该线程

    Android、IOS等cook版本为Async模式,会开启该线程

    bool IsInAsyncLoadingThread(); --》bool IsInAsyncLoadingThreadCoreUObjectInternal()

    bool IsAsyncLoading(); --》bool IsAsyncLoadingCoreUObjectInternal()

    void SuspendAsyncLoading(); --》void SuspendAsyncLoadingInternal()

    void ResumeAsyncLoading(); --》void ResumeAsyncLoadingInternal()

    bool IsAsyncLoadingSuspended(); --》bool IsAsyncLoadingSuspendedInternal()

    bool IsAsyncLoadingMultithreaded(); --》bool IsAsyncLoadingMultithreadedCoreUObjectInternal()

    class FAsyncLoadingThread2 final : public FRunnable, public IAsyncPackageLoader   暂时没用
    class FAsyncLoadingThreadWorker : private FRunnable   暂时没用

    防屏保线程

    class FScreenSaverInhibitor : public FRunnable

    桌面平台

    宏PLATFORM_DESKTOP为1

    Windows、Linux、Mac

    线程名:ScreenSaverInhibitor

    StatsThread线程(消耗高)

    class FStatsThread : public FRunnable, FSingleThreadRunnable

     All

    线程名:StatsThread

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::StatsThread)

    FMediaTicker线程

    class FMediaTicker : public FRunnable , public IMediaTicker

     All 线程名:FMediaTicker

    class FAsyncPurge : public FRunnable

     All

    extern int32 GMultithreadedDestructionEnabled;

    [/Script/Engine.GarbageCollectionSettings]

    gc.MultithreadedDestructionEnabled=True // 通过这个控制台命令来开启

    多线程GC清理


    class FSlateLoadingThreadTask : public FRunnable

     All

    线程名:SlateLoadingThread1

    LoadingScreen播放视频或Slate UI

    bool IsInSlateThread();

    class  FPreLoadScreenSlateThreadTask : public FRunnable  All

    线程名:SlateLoadingThread1

    引擎初始化播放视频

    bool IsInSlateThread();

    template<typename ResultType>
    class TAsyncRunnable : public FRunnable

     All TAsync 0
    class FAsyncWriter : public FRunnable, public FArchive  All

    FAsyncWriter_UAGame

    见:OutputDeviceFile.cpp的FAsyncWriter::Run()

    异步写log到文件

    class FOnlineAsyncTaskManager : public FRunnable, FSingleThreadRunnable  All OnlineAsyncTaskThreadNull DefaultInstance(1)
    class FLwsWebSocketsManager: public IWebSocketsManager, public FRunnable, public FSingleThreadRunnable  All LibwebsocketsThread
    class FHttpThread : FRunnable, FSingleThreadRunnable  All HttpManagerThread
    class FMessageRouter : public FRunnable, private FSingleThreadRunnable  All FMessageBus.DefaultBus.Router
    class FLiveLinkMessageBusDiscoveryManager : FRunnable  All LiveLinkMessageBusDiscoveryManager
    class FFileTransferRunnable : public FRunnable  All FFileTransferRunnable
    class TcpConsoleListener : FRunnable iOS  
    class FTcpListener : public FRunnable iOS  

    @implementation FIOSFramePacer

    -(void)run:(id)param

    iOS  
    windows平台Splash线程 windows 线程函数:StartSplashScreenThread
    TraceLog windows

    windows:线程名为TraceLog;WindowsTrace.cpp下ThreadCreate函数

    Android:线程名为bundle id;AndroidTrace.cpp下ThreadCreate函数

    IOS:线程名为Thread <n>;AppleTrace.cpp下ThreadCreate函数

    windows平台崩溃监视线程 windows 线程函数:CrashReportingThreadProc

    Shader编译线程

    class FShaderCompileThreadRunnableBase : public FRunnable

    编辑器

    线程名:ShaderCompilingThread

    拉起ShaderCompileWorker.exe进程进行shader编译

    DistanceField构建线程

    class FBuildDistanceFieldThreadRunnable : public FRunnable

    编辑器  

    class FAssetDataDiscovery : public FRunnable

    编辑器

    用于发现文件

    线程名:FAssetDataDiscovery

    class FAssetDataGatherer : public FRunnable

    编辑器

    从FAssetRegistry文件列表中搜集Asset数据

    线程名:FAssetDataGatherer

    class FVirtualTextureDCCCacheCleanup final : public FRunnable

    编辑器 线程名:FVirtualTextureDCCCacheCleanup
    class FDDCCleanup : public FRunnable 编辑器 线程名:FDDCCleanup

    Wwise编辑器连接线程

    class FAkWaapiClientConnectionHandler : public FRunnable

    编辑器或windows、mac下的非shipping版本

    线程名:WAAPIClientConnectionThread1

    在Android局内时抓的一个ue4stats文件中,里面统计的线程如下:

    参考

    【UE4源代码观察】观察UE4的线程使用

  • 相关阅读:
    .net注册iis
    hdu 1081To The Max
    hdu 1312Red and Black
    hdu 1016Prime Ring Problem
    hdu 1159Common Subsequence
    hdu 1372Knight Moves
    hdu 1686Oulipo
    hdu 1241Oil Deposits
    hdu 1171Big Event in HDU
    hdu 4006The kth great number
  • 原文地址:https://www.cnblogs.com/kekec/p/13732486.html
Copyright © 2011-2022 走看看